From 811d3e1c338def37e0bb4a5017457d4f74ae3651 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 25 Feb 2026 11:31:44 +0100 Subject: [PATCH] feat(server): ocr sync --- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + .../openapi/lib/model/sync_asset_ocr_v1.dart | 217 ++++++++++++++++++ .../openapi/lib/model/sync_entity_type.dart | 3 + .../openapi/lib/model/sync_request_type.dart | 3 + open-api/immich-openapi-specs.json | 79 +++++++ open-api/typescript-sdk/src/fetch-client.ts | 32 +++ server/src/database.ts | 17 ++ server/src/dtos/sync.dto.ts | 46 ++++ server/src/enum.ts | 2 + server/src/repositories/sync.repository.ts | 13 ++ .../1772014962747-AssetOcrUpdateId.ts | 11 + server/src/schema/tables/asset-ocr.table.ts | 4 + server/src/services/sync.service.ts | 18 ++ 15 files changed, 449 insertions(+) create mode 100644 mobile/openapi/lib/model/sync_asset_ocr_v1.dart create mode 100644 server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 02daa8543d..52e88c3f93 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -582,6 +582,7 @@ Class | Method | HTTP request | Description - [SyncAssetFaceV2](doc//SyncAssetFaceV2.md) - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) + - [SyncAssetOcrV1](doc//SyncAssetOcrV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) - [SyncAuthUserV1](doc//SyncAuthUserV1.md) - [SyncEntityType](doc//SyncEntityType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index bfbe829d8d..d13994a3bd 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -321,6 +321,7 @@ part 'model/sync_asset_face_v1.dart'; part 'model/sync_asset_face_v2.dart'; part 'model/sync_asset_metadata_delete_v1.dart'; part 'model/sync_asset_metadata_v1.dart'; +part 'model/sync_asset_ocr_v1.dart'; part 'model/sync_asset_v1.dart'; part 'model/sync_auth_user_v1.dart'; part 'model/sync_entity_type.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index e703542b52..a7699b4288 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -688,6 +688,8 @@ class ApiClient { return SyncAssetMetadataDeleteV1.fromJson(value); case 'SyncAssetMetadataV1': return SyncAssetMetadataV1.fromJson(value); + case 'SyncAssetOcrV1': + return SyncAssetOcrV1.fromJson(value); case 'SyncAssetV1': return SyncAssetV1.fromJson(value); case 'SyncAuthUserV1': diff --git a/mobile/openapi/lib/model/sync_asset_ocr_v1.dart b/mobile/openapi/lib/model/sync_asset_ocr_v1.dart new file mode 100644 index 0000000000..62b4741b5b --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_ocr_v1.dart @@ -0,0 +1,217 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetOcrV1 { + /// Returns a new [SyncAssetOcrV1] instance. + SyncAssetOcrV1({ + required this.assetId, + required this.boxScore, + required this.id, + required this.isVisible, + required this.text, + required this.textScore, + required this.x1, + required this.x2, + required this.x3, + required this.x4, + required this.y1, + required this.y2, + required this.y3, + required this.y4, + }); + + /// Asset ID + String assetId; + + /// Confidence score of the bounding box + num boxScore; + + /// OCR entry ID + String id; + + /// Whether the OCR entry is visible + bool isVisible; + + /// Recognized text content + String text; + + /// Confidence score of the recognized text + num textScore; + + /// Top-left X coordinate (normalized 0–1) + num x1; + + /// Top-right X coordinate (normalized 0–1) + num x2; + + /// Bottom-right X coordinate (normalized 0–1) + num x3; + + /// Bottom-left X coordinate (normalized 0–1) + num x4; + + /// Top-left Y coordinate (normalized 0–1) + num y1; + + /// Top-right Y coordinate (normalized 0–1) + num y2; + + /// Bottom-right Y coordinate (normalized 0–1) + num y3; + + /// Bottom-left Y coordinate (normalized 0–1) + num y4; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrV1 && + other.assetId == assetId && + other.boxScore == boxScore && + other.id == id && + other.isVisible == isVisible && + other.text == text && + other.textScore == textScore && + other.x1 == x1 && + other.x2 == x2 && + other.x3 == x3 && + other.x4 == x4 && + other.y1 == y1 && + other.y2 == y2 && + other.y3 == y3 && + other.y4 == y4; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (boxScore.hashCode) + + (id.hashCode) + + (isVisible.hashCode) + + (text.hashCode) + + (textScore.hashCode) + + (x1.hashCode) + + (x2.hashCode) + + (x3.hashCode) + + (x4.hashCode) + + (y1.hashCode) + + (y2.hashCode) + + (y3.hashCode) + + (y4.hashCode); + + @override + String toString() => 'SyncAssetOcrV1[assetId=$assetId, boxScore=$boxScore, id=$id, isVisible=$isVisible, text=$text, textScore=$textScore, x1=$x1, x2=$x2, x3=$x3, x4=$x4, y1=$y1, y2=$y2, y3=$y3, y4=$y4]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'boxScore'] = this.boxScore; + json[r'id'] = this.id; + json[r'isVisible'] = this.isVisible; + json[r'text'] = this.text; + json[r'textScore'] = this.textScore; + json[r'x1'] = this.x1; + json[r'x2'] = this.x2; + json[r'x3'] = this.x3; + json[r'x4'] = this.x4; + json[r'y1'] = this.y1; + json[r'y2'] = this.y2; + json[r'y3'] = this.y3; + json[r'y4'] = this.y4; + return json; + } + + /// Returns a new [SyncAssetOcrV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetOcrV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetOcrV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetOcrV1( + assetId: mapValueOfType(json, r'assetId')!, + boxScore: num.parse('${json[r'boxScore']}'), + id: mapValueOfType(json, r'id')!, + isVisible: mapValueOfType(json, r'isVisible')!, + text: mapValueOfType(json, r'text')!, + textScore: num.parse('${json[r'textScore']}'), + x1: num.parse('${json[r'x1']}'), + x2: num.parse('${json[r'x2']}'), + x3: num.parse('${json[r'x3']}'), + x4: num.parse('${json[r'x4']}'), + y1: num.parse('${json[r'y1']}'), + y2: num.parse('${json[r'y2']}'), + y3: num.parse('${json[r'y3']}'), + y4: num.parse('${json[r'y4']}'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetOcrV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetOcrV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetOcrV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetOcrV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'boxScore', + 'id', + 'isVisible', + 'text', + 'textScore', + 'x1', + 'x2', + 'x3', + 'x4', + 'y1', + 'y2', + 'y3', + 'y4', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index e7605a5dd1..e05d027c16 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -31,6 +31,7 @@ class SyncEntityType { static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); + static const assetOcrV1 = SyncEntityType._(r'AssetOcrV1'); static const partnerV1 = SyncEntityType._(r'PartnerV1'); static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); @@ -82,6 +83,7 @@ class SyncEntityType { assetExifV1, assetMetadataV1, assetMetadataDeleteV1, + assetOcrV1, partnerV1, partnerDeleteV1, partnerAssetV1, @@ -168,6 +170,7 @@ class SyncEntityTypeTypeTransformer { case r'AssetExifV1': return SyncEntityType.assetExifV1; case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; + case r'AssetOcrV1': return SyncEntityType.assetOcrV1; case r'PartnerV1': return SyncEntityType.partnerV1; case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 3614394d55..c1ec705edb 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -31,6 +31,7 @@ class SyncRequestType { static const assetsV1 = SyncRequestType._(r'AssetsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); + static const assetOcrV1 = SyncRequestType._(r'AssetOcrV1'); static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); @@ -55,6 +56,7 @@ class SyncRequestType { assetsV1, assetExifsV1, assetMetadataV1, + assetOcrV1, authUsersV1, memoriesV1, memoryToAssetsV1, @@ -114,6 +116,7 @@ class SyncRequestTypeTypeTransformer { case r'AssetsV1': return SyncRequestType.assetsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1; case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; + case r'AssetOcrV1': return SyncRequestType.assetOcrV1; case r'AuthUsersV1': return SyncRequestType.authUsersV1; case r'MemoriesV1': return SyncRequestType.memoriesV1; case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f7702b0ce4..4b96cd3a80 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -22944,6 +22944,83 @@ ], "type": "object" }, + "SyncAssetOcrV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "type": "string" + }, + "boxScore": { + "description": "Confidence score of the bounding box", + "type": "number" + }, + "id": { + "description": "OCR entry ID", + "type": "string" + }, + "isVisible": { + "description": "Whether the OCR entry is visible", + "type": "boolean" + }, + "text": { + "description": "Recognized text content", + "type": "string" + }, + "textScore": { + "description": "Confidence score of the recognized text", + "type": "number" + }, + "x1": { + "description": "Top-left X coordinate (normalized 0–1)", + "type": "number" + }, + "x2": { + "description": "Top-right X coordinate (normalized 0–1)", + "type": "number" + }, + "x3": { + "description": "Bottom-right X coordinate (normalized 0–1)", + "type": "number" + }, + "x4": { + "description": "Bottom-left X coordinate (normalized 0–1)", + "type": "number" + }, + "y1": { + "description": "Top-left Y coordinate (normalized 0–1)", + "type": "number" + }, + "y2": { + "description": "Top-right Y coordinate (normalized 0–1)", + "type": "number" + }, + "y3": { + "description": "Bottom-right Y coordinate (normalized 0–1)", + "type": "number" + }, + "y4": { + "description": "Bottom-left Y coordinate (normalized 0–1)", + "type": "number" + } + }, + "required": [ + "assetId", + "boxScore", + "id", + "isVisible", + "text", + "textScore", + "x1", + "x2", + "x3", + "x4", + "y1", + "y2", + "y3", + "y4" + ], + "type": "object" + }, "SyncAssetV1": { "properties": { "checksum": { @@ -23165,6 +23242,7 @@ "AssetExifV1", "AssetMetadataV1", "AssetMetadataDeleteV1", + "AssetOcrV1", "PartnerV1", "PartnerDeleteV1", "PartnerAssetV1", @@ -23461,6 +23539,7 @@ "AssetsV1", "AssetExifsV1", "AssetMetadataV1", + "AssetOcrV1", "AuthUsersV1", "MemoriesV1", "MemoryToAssetsV1", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7c1f940a91..63d3e32ed4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -3073,6 +3073,36 @@ export type SyncAssetMetadataV1 = { /** Value */ value: object; }; +export type SyncAssetOcrV1 = { + /** Asset ID */ + assetId: string; + /** Confidence score of the bounding box */ + boxScore: number; + /** OCR entry ID */ + id: string; + /** Whether the OCR entry is visible */ + isVisible: boolean; + /** Recognized text content */ + text: string; + /** Confidence score of the recognized text */ + textScore: number; + /** Top-left X coordinate (normalized 0–1) */ + x1: number; + /** Top-right X coordinate (normalized 0–1) */ + x2: number; + /** Bottom-right X coordinate (normalized 0–1) */ + x3: number; + /** Bottom-left X coordinate (normalized 0–1) */ + x4: number; + /** Top-left Y coordinate (normalized 0–1) */ + y1: number; + /** Top-right Y coordinate (normalized 0–1) */ + y2: number; + /** Bottom-right Y coordinate (normalized 0–1) */ + y3: number; + /** Bottom-left Y coordinate (normalized 0–1) */ + y4: number; +}; export type SyncAssetV1 = { /** Checksum */ checksum: string; @@ -7232,6 +7262,7 @@ export enum SyncEntityType { AssetExifV1 = "AssetExifV1", AssetMetadataV1 = "AssetMetadataV1", AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", + AssetOcrV1 = "AssetOcrV1", PartnerV1 = "PartnerV1", PartnerDeleteV1 = "PartnerDeleteV1", PartnerAssetV1 = "PartnerAssetV1", @@ -7282,6 +7313,7 @@ export enum SyncRequestType { AssetsV1 = "AssetsV1", AssetExifsV1 = "AssetExifsV1", AssetMetadataV1 = "AssetMetadataV1", + AssetOcrV1 = "AssetOcrV1", AuthUsersV1 = "AuthUsersV1", MemoriesV1 = "MemoriesV1", MemoryToAssetsV1 = "MemoryToAssetsV1", diff --git a/server/src/database.ts b/server/src/database.ts index 5d29cbb043..030a8c71df 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -437,6 +437,23 @@ export const columns = { 'asset_exif.rating', 'asset_exif.fps', ], + syncAssetOcr: [ + 'id', + 'assetId', + 'x1', + 'y1', + 'x2', + 'y2', + 'x3', + 'y3', + 'x4', + 'y4', + 'text', + 'boxScore', + 'textScore', + 'updateId', + 'isVisible', + ], exif: [ 'asset_exif.assetId', 'asset_exif.autoStackId', diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index c1b85c0430..35023a45d7 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -218,6 +218,51 @@ export class SyncAssetExifV1 { fps!: number | null; } +@ExtraModel() +export class SyncAssetOcrV1 { + @ApiProperty({ description: 'OCR entry ID' }) + id!: string; + + @ApiProperty({ description: 'Asset ID' }) + assetId!: string; + + @ApiProperty({ description: 'Top-left X coordinate (normalized 0–1)' }) + x1!: number; + + @ApiProperty({ description: 'Top-left Y coordinate (normalized 0–1)' }) + y1!: number; + + @ApiProperty({ description: 'Top-right X coordinate (normalized 0–1)' }) + x2!: number; + + @ApiProperty({ description: 'Top-right Y coordinate (normalized 0–1)' }) + y2!: number; + + @ApiProperty({ description: 'Bottom-right X coordinate (normalized 0–1)' }) + x3!: number; + + @ApiProperty({ description: 'Bottom-right Y coordinate (normalized 0–1)' }) + y3!: number; + + @ApiProperty({ description: 'Bottom-left X coordinate (normalized 0–1)' }) + x4!: number; + + @ApiProperty({ description: 'Bottom-left Y coordinate (normalized 0–1)' }) + y4!: number; + + @ApiProperty({ description: 'Confidence score of the bounding box' }) + boxScore!: number; + + @ApiProperty({ description: 'Confidence score of the recognized text' }) + textScore!: number; + + @ApiProperty({ description: 'Recognized text content' }) + text!: string; + + @ApiProperty({ description: 'Whether the OCR entry is visible' }) + isVisible!: boolean; +} + @ExtraModel() export class SyncAssetMetadataV1 { @ApiProperty({ description: 'Asset ID' }) @@ -480,6 +525,7 @@ export type SyncItem = { [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; [SyncEntityType.AssetExifV1]: SyncAssetExifV1; + [SyncEntityType.AssetOcrV1]: SyncAssetOcrV1; [SyncEntityType.PartnerAssetV1]: SyncAssetV1; [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; [SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1; diff --git a/server/src/enum.ts b/server/src/enum.ts index 802b3c96e0..ca5d22cbeb 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -721,6 +721,7 @@ export enum SyncRequestType { AssetsV1 = 'AssetsV1', AssetExifsV1 = 'AssetExifsV1', AssetMetadataV1 = 'AssetMetadataV1', + AssetOcrV1 = 'AssetOcrV1', AuthUsersV1 = 'AuthUsersV1', MemoriesV1 = 'MemoriesV1', MemoryToAssetsV1 = 'MemoryToAssetsV1', @@ -747,6 +748,7 @@ export enum SyncEntityType { AssetExifV1 = 'AssetExifV1', AssetMetadataV1 = 'AssetMetadataV1', AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', + AssetOcrV1 = 'AssetOcrV1', PartnerV1 = 'PartnerV1', PartnerDeleteV1 = 'PartnerDeleteV1', diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index f851038dc6..a2c5f19193 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -55,6 +55,7 @@ export class SyncRepository { assetExif: AssetExifSync; assetFace: AssetFaceSync; assetMetadata: AssetMetadataSync; + assetOcr: AssetOcrSync; authUser: AuthUserSync; memory: MemorySync; memoryToAsset: MemoryToAssetSync; @@ -77,6 +78,7 @@ export class SyncRepository { this.assetExif = new AssetExifSync(this.db); this.assetFace = new AssetFaceSync(this.db); this.assetMetadata = new AssetMetadataSync(this.db); + this.assetOcr = new AssetOcrSync(this.db); this.authUser = new AuthUserSync(this.db); this.memory = new MemorySync(this.db); this.memoryToAsset = new MemoryToAssetSync(this.db); @@ -772,3 +774,14 @@ class AssetMetadataSync extends BaseSync { .stream(); } } + +class AssetOcrSync extends BaseSync { + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getUpserts(options: SyncQueryOptions, userId: string) { + return this.upsertQuery('asset_ocr', options) + .select(columns.syncAssetOcr) + .innerJoin('asset', 'asset.id', 'asset_ocr.assetId') + .where('asset.ownerId', '=', userId) + .stream(); + } +} diff --git a/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts b/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts new file mode 100644 index 0000000000..b17f93363a --- /dev/null +++ b/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_ocr" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`CREATE INDEX "asset_ocr_updateId_idx" ON "asset_ocr" ("updateId");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "asset_ocr_updateId_idx";`.execute(db); + await sql`ALTER TABLE "asset_ocr" DROP COLUMN "updateId";`.execute(db); +} diff --git a/server/src/schema/tables/asset-ocr.table.ts b/server/src/schema/tables/asset-ocr.table.ts index b58224a247..54946f037a 100644 --- a/server/src/schema/tables/asset-ocr.table.ts +++ b/server/src/schema/tables/asset-ocr.table.ts @@ -1,4 +1,5 @@ import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; +import { UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; @Table('asset_ocr') @@ -45,4 +46,7 @@ export class AssetOcrTable { @Column({ type: 'boolean', default: true }) isVisible!: Generated; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 76fd129f50..dfe4826457 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -193,6 +193,7 @@ export class SyncService extends BaseService { [SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap), [SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap), [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap), + [SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth), }; for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { @@ -855,6 +856,23 @@ export class SyncService extends BaseService { } } + private async syncAssetOcrV1( + options: SyncQueryOptions, + response: Writable, + checkpointMap: CheckpointMap, + auth: AuthDto, + ) { + const upsertType = SyncEntityType.AssetOcrV1; + const upserts = this.syncRepository.assetOcr.getUpserts( + { ...options, ack: checkpointMap[upsertType] }, + auth.user.id, + ); + + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { const { type, sessionId, createId } = item; await this.syncCheckpointRepository.upsertAll([