mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
feat: add OCR functionality and related configurations
This commit is contained in:
parent
23fb2e0fae
commit
0e8ca1c159
64 changed files with 3998 additions and 1669 deletions
|
|
@ -131,7 +131,7 @@ services:
|
|||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- ../machine-learning:/usr/src/app
|
||||
- ../machine-learning/immich_ml:/usr/src/immich_ml
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
- .env
|
||||
|
|
|
|||
13
i18n/en.json
13
i18n/en.json
|
|
@ -145,6 +145,14 @@
|
|||
"machine_learning_min_detection_score_description": "Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.",
|
||||
"machine_learning_min_recognized_faces": "Minimum recognized faces",
|
||||
"machine_learning_min_recognized_faces_description": "The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.",
|
||||
"machine_learning_ocr": "OCR",
|
||||
"machine_learning_ocr_description": "Use machine learning to recognize text in images",
|
||||
"machine_learning_ocr_enabled": "Enable OCR",
|
||||
"machine_learning_ocr_enabled_description": "If disabled, images will not be encoded for text recognition.",
|
||||
"machine_learning_ocr_model": "OCR model",
|
||||
"machine_learning_ocr_model_description": "Choose an OCR model.",
|
||||
"machine_learning_ocr_min_score": "Minimum recognition score",
|
||||
"machine_learning_ocr_min_score_description": "Minimum confidence score for text to be recognized from 0-1. Lower values will recognize more text but may result in false positives.",
|
||||
"machine_learning_settings": "Machine Learning Settings",
|
||||
"machine_learning_settings_description": "Manage machine learning features and settings",
|
||||
"machine_learning_smart_search": "Smart Search",
|
||||
|
|
@ -232,6 +240,7 @@
|
|||
"oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.",
|
||||
"oauth_storage_quota_default": "Default storage quota (GiB)",
|
||||
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided.",
|
||||
"ocr_job_description": "Use machine learning to recognize text in images",
|
||||
"oauth_timeout": "Request Timeout",
|
||||
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
||||
"password_enable_description": "Login with email and password",
|
||||
|
|
@ -1396,6 +1405,7 @@
|
|||
"notifications": "Notifications",
|
||||
"notifications_setting_description": "Manage notifications",
|
||||
"oauth": "OAuth",
|
||||
"ocr": "OCR",
|
||||
"official_immich_resources": "Official Immich Resources",
|
||||
"offline": "Offline",
|
||||
"offset": "Offset",
|
||||
|
|
@ -1670,6 +1680,8 @@
|
|||
"search_by_description_example": "Hiking day in Sapa",
|
||||
"search_by_filename": "Search by file name or extension",
|
||||
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
||||
"search_by_ocr": "Search by OCR",
|
||||
"search_by_ocr_example": "Latte",
|
||||
"search_camera_make": "Search camera make...",
|
||||
"search_camera_model": "Search camera model...",
|
||||
"search_city": "Search city...",
|
||||
|
|
@ -1686,6 +1698,7 @@
|
|||
"search_filter_location_title": "Select location",
|
||||
"search_filter_media_type": "Media Type",
|
||||
"search_filter_media_type_title": "Select media type",
|
||||
"search_filter_ocr": "Search by OCR",
|
||||
"search_filter_people_title": "Select people",
|
||||
"search_for": "Search for",
|
||||
"search_for_existing_person": "Search for existing person",
|
||||
|
|
|
|||
|
|
@ -145,6 +145,14 @@
|
|||
"machine_learning_min_detection_score_description": "检测到人脸的最小置信分数为0-1。较低的值将检测到更多人脸,但可能导致误报。",
|
||||
"machine_learning_min_recognized_faces": "识别的最少人脸数",
|
||||
"machine_learning_min_recognized_faces_description": "创建一个人所需识别的最少人脸数量。提高这个值可以使人脸识别更精确,但也增加了人脸未能被分配到相对应人物的可能性。",
|
||||
"machine_learning_ocr": "文本识别",
|
||||
"machine_learning_ocr_description": "使用机器学习识别图片中的文本",
|
||||
"machine_learning_ocr_enabled": "启用文本识别",
|
||||
"machine_learning_ocr_enabled_description": "如果禁用,则不会对图像编码以用于文本识别。",
|
||||
"machine_learning_ocr_model": "文本识别模型",
|
||||
"machine_learning_ocr_model_description": "选择一个文本识别模型。",
|
||||
"machine_learning_ocr_min_score": "最低识别分数",
|
||||
"machine_learning_ocr_min_score_description": "文本识别的最小置信度分数范围是0到1。较低的值将识别出更多的文本,但可能导致误报。",
|
||||
"machine_learning_settings": "机器学习设置",
|
||||
"machine_learning_settings_description": "管理机器学习功能和设置",
|
||||
"machine_learning_smart_search": "智能搜索",
|
||||
|
|
@ -234,6 +242,7 @@
|
|||
"oauth_storage_quota_default_description": "未提供声明时使用的配额(GiB)。",
|
||||
"oauth_timeout": "请求超时",
|
||||
"oauth_timeout_description": "请求超时(毫秒)",
|
||||
"ocr_job_description": "使用机器学习识别图片中的文本",
|
||||
"password_enable_description": "使用邮箱和密码登录",
|
||||
"password_settings": "密码登录",
|
||||
"password_settings_description": "管理密码登录设置",
|
||||
|
|
@ -1396,6 +1405,7 @@
|
|||
"notifications": "通知",
|
||||
"notifications_setting_description": "管理通知",
|
||||
"oauth": "OAuth",
|
||||
"ocr": "文本识别",
|
||||
"official_immich_resources": "Immich 官方资源",
|
||||
"offline": "离线",
|
||||
"offset": "偏移量",
|
||||
|
|
@ -1670,6 +1680,8 @@
|
|||
"search_by_description_example": "在沙巴徒步的日子",
|
||||
"search_by_filename": "按文件名或扩展名查找",
|
||||
"search_by_filename_example": "如 IMG_1234.JPG 或 PNG",
|
||||
"search_by_ocr": "通过文本识别查找",
|
||||
"search_by_ocr_example": "拿铁咖啡",
|
||||
"search_camera_make": "按相机品牌查找...",
|
||||
"search_camera_model": "按相机型号查找...",
|
||||
"search_city": "按城市查找...",
|
||||
|
|
@ -1686,6 +1698,7 @@
|
|||
"search_filter_location_title": "选择位置",
|
||||
"search_filter_media_type": "媒体类型",
|
||||
"search_filter_media_type_title": "选择媒体类型",
|
||||
"search_filter_ocr": "通过文本识别搜索",
|
||||
"search_filter_people_title": "选择人物",
|
||||
"search_for": "查找",
|
||||
"search_for_existing_person": "查找已有人物",
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ FROM prod-${DEVICE} AS prod
|
|||
ARG DEVICE
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends tini $(if ! [ "$DEVICE" = "openvino" ] && ! [ "$DEVICE" = "rocm" ]; then echo "libmimalloc2.0"; fi) && \
|
||||
apt-get install -y --no-install-recommends tini libgl1 libglib2.0-0 libgomp1 $(if ! [ "$DEVICE" = "openvino" ] && ! [ "$DEVICE" = "rocm" ]; then echo "libmimalloc2.0"; fi) && \
|
||||
apt-get autoremove -yqq && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from immich_ml.schemas import ModelSource, ModelTask, ModelType
|
|||
from .constants import get_model_source
|
||||
from .facial_recognition.detection import FaceDetector
|
||||
from .facial_recognition.recognition import FaceRecognizer
|
||||
|
||||
from .ocr.paddle import PaddleOCRecognizer
|
||||
|
||||
def get_model_class(model_name: str, model_type: ModelType, model_task: ModelTask) -> type[InferenceModel]:
|
||||
source = get_model_source(model_name)
|
||||
|
|
@ -28,6 +28,9 @@ def get_model_class(model_name: str, model_type: ModelType, model_task: ModelTas
|
|||
case ModelSource.INSIGHTFACE, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION:
|
||||
return FaceRecognizer
|
||||
|
||||
case ModelSource.PADDLE, ModelType.OCR, ModelTask.OCR:
|
||||
return PaddleOCRecognizer
|
||||
|
||||
case _:
|
||||
raise ValueError(f"Unknown model combination: {source}, {model_type}, {model_task}")
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ _INSIGHTFACE_MODELS = {
|
|||
}
|
||||
|
||||
|
||||
_PADDLE_MODELS = {
|
||||
"paddle",
|
||||
}
|
||||
|
||||
SUPPORTED_PROVIDERS = [
|
||||
"CUDAExecutionProvider",
|
||||
"ROCMExecutionProvider",
|
||||
|
|
@ -158,4 +162,7 @@ def get_model_source(model_name: str) -> ModelSource | None:
|
|||
if cleaned_name in _OPENCLIP_MODELS:
|
||||
return ModelSource.OPENCLIP
|
||||
|
||||
if cleaned_name in _PADDLE_MODELS:
|
||||
return ModelSource.PADDLE
|
||||
|
||||
return None
|
||||
|
|
|
|||
47
machine-learning/immich_ml/models/ocr/paddle.py
Normal file
47
machine-learning/immich_ml/models/ocr/paddle.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
from paddleocr import PaddleOCR
|
||||
from PIL import Image
|
||||
from immich_ml.models.base import InferenceModel
|
||||
from immich_ml.models.transforms import decode_cv2
|
||||
from immich_ml.schemas import OCROutput, ModelTask, ModelType
|
||||
|
||||
class PaddleOCRecognizer(InferenceModel):
|
||||
depends = []
|
||||
identity = (ModelType.OCR, ModelTask.OCR)
|
||||
|
||||
def __init__(self, model_name: str, min_score: float = 0.9, **model_kwargs: Any) -> None:
|
||||
self.min_score = model_kwargs.pop("minScore", min_score)
|
||||
super().__init__(model_name, **model_kwargs)
|
||||
self._load()
|
||||
self.loaded = True
|
||||
|
||||
def _load(self) -> None:
|
||||
try:
|
||||
self.model = PaddleOCR(
|
||||
use_doc_orientation_classify=False,
|
||||
use_doc_unwarping=False,
|
||||
use_textline_orientation=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading PaddleOCR model: {e}")
|
||||
raise e
|
||||
|
||||
def _predict(self, inputs: NDArray[np.uint8] | bytes | Image.Image, **kwargs: Any) -> OCROutput:
|
||||
inputs = decode_cv2(inputs)
|
||||
results = self.model.predict(inputs)
|
||||
valid_texts_and_scores = [
|
||||
(text, score)
|
||||
for result in results
|
||||
for text, score in zip(result['rec_texts'], result['rec_scores'])
|
||||
if score > self.min_score
|
||||
]
|
||||
if not valid_texts_and_scores:
|
||||
return OCROutput(text="", confidence=0.0)
|
||||
texts, scores = zip(*valid_texts_and_scores)
|
||||
return OCROutput(
|
||||
text="".join(texts),
|
||||
confidence=sum(scores) / len(scores)
|
||||
)
|
||||
|
|
@ -23,14 +23,14 @@ class BoundingBox(TypedDict):
|
|||
class ModelTask(StrEnum):
|
||||
FACIAL_RECOGNITION = "facial-recognition"
|
||||
SEARCH = "clip"
|
||||
|
||||
OCR = "ocr"
|
||||
|
||||
class ModelType(StrEnum):
|
||||
DETECTION = "detection"
|
||||
RECOGNITION = "recognition"
|
||||
TEXTUAL = "textual"
|
||||
VISUAL = "visual"
|
||||
|
||||
OCR = "ocr"
|
||||
|
||||
class ModelFormat(StrEnum):
|
||||
ARMNN = "armnn"
|
||||
|
|
@ -42,7 +42,7 @@ class ModelSource(StrEnum):
|
|||
INSIGHTFACE = "insightface"
|
||||
MCLIP = "mclip"
|
||||
OPENCLIP = "openclip"
|
||||
|
||||
PADDLE = "paddle"
|
||||
|
||||
ModelIdentity = tuple[ModelType, ModelTask]
|
||||
|
||||
|
|
@ -87,6 +87,11 @@ class DetectedFace(TypedDict):
|
|||
FacialRecognitionOutput = list[DetectedFace]
|
||||
|
||||
|
||||
class OCROutput(TypedDict):
|
||||
text: str
|
||||
confidence: float
|
||||
|
||||
|
||||
class PipelineEntry(TypedDict):
|
||||
modelName: str
|
||||
options: dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ dependencies = [
|
|||
"rich>=13.4.2",
|
||||
"tokenizers>=0.15.0,<1.0",
|
||||
"uvicorn[standard]>=0.22.0,<1.0",
|
||||
"paddleocr>=3.0.0",
|
||||
"setuptools>=78.1.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
@ -48,11 +50,15 @@ lint = [
|
|||
dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
|
||||
|
||||
[project.optional-dependencies]
|
||||
cpu = ["onnxruntime>=1.15.0,<2"]
|
||||
cuda = ["onnxruntime-gpu>=1.17.0,<2"]
|
||||
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
|
||||
armnn = ["onnxruntime>=1.15.0,<2"]
|
||||
rknn = ["onnxruntime>=1.15.0,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
|
||||
cpu = ["onnxruntime>=1.15.0,<2", "paddlepaddle==3.0.0rc1"]
|
||||
cuda = ["onnxruntime-gpu>=1.17.0,<2", "paddlepaddle-gpu==3.0.0rc1"]
|
||||
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0", "paddlepaddle==3.0.0rc1"]
|
||||
armnn = ["onnxruntime>=1.15.0,<2", "paddlepaddle==3.0.0rc1"]
|
||||
rknn = [
|
||||
"onnxruntime>=1.15.0,<2",
|
||||
"rknn-toolkit-lite2>=2.3.0,<3",
|
||||
"paddlepaddle==3.0.0rc1",
|
||||
]
|
||||
rocm = []
|
||||
|
||||
[tool.uv]
|
||||
|
|
@ -63,8 +69,20 @@ name = "cuda12"
|
|||
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "paddlepaddle-cpu"
|
||||
url = "https://www.paddlepaddle.org.cn/packages/stable/cpu/"
|
||||
explicit = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "paddlepaddle-gpu"
|
||||
url = "https://www.paddlepaddle.org.cn/packages/stable/cu118/"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
onnxruntime-gpu = { index = "cuda12" }
|
||||
paddlepaddle = { index = "paddlepaddle-cpu" }
|
||||
paddlepaddle-gpu = { index = "paddlepaddle-gpu" }
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["immich_ml"]
|
||||
|
|
|
|||
3932
machine-learning/uv.lock
generated
3932
machine-learning/uv.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
enum SortOrder { asc, desc }
|
||||
|
||||
enum TextSearchType { context, filename, description }
|
||||
enum TextSearchType { context, filename, description, ocr }
|
||||
|
||||
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ class SearchFilter {
|
|||
String? context;
|
||||
String? filename;
|
||||
String? description;
|
||||
String? ocr;
|
||||
String? language;
|
||||
Set<PersonDto> people;
|
||||
SearchLocationFilter location;
|
||||
|
|
@ -190,6 +191,7 @@ class SearchFilter {
|
|||
this.context,
|
||||
this.filename,
|
||||
this.description,
|
||||
this.ocr,
|
||||
this.language,
|
||||
required this.people,
|
||||
required this.location,
|
||||
|
|
@ -203,6 +205,7 @@ class SearchFilter {
|
|||
return (context == null || (context != null && context!.isEmpty)) &&
|
||||
(filename == null || (filename!.isEmpty)) &&
|
||||
(description == null || (description!.isEmpty)) &&
|
||||
(ocr == null || (ocr!.isEmpty)) &&
|
||||
people.isEmpty &&
|
||||
location.country == null &&
|
||||
location.state == null &&
|
||||
|
|
@ -222,6 +225,7 @@ class SearchFilter {
|
|||
String? filename,
|
||||
String? description,
|
||||
String? language,
|
||||
String? ocr,
|
||||
Set<PersonDto>? people,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
|
|
@ -234,6 +238,7 @@ class SearchFilter {
|
|||
filename: filename ?? this.filename,
|
||||
description: description ?? this.description,
|
||||
language: language ?? this.language,
|
||||
ocr: ocr ?? this.ocr,
|
||||
people: people ?? this.people,
|
||||
location: location ?? this.location,
|
||||
camera: camera ?? this.camera,
|
||||
|
|
@ -245,7 +250,7 @@ class SearchFilter {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
|
||||
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -256,6 +261,7 @@ class SearchFilter {
|
|||
other.filename == filename &&
|
||||
other.description == description &&
|
||||
other.language == language &&
|
||||
other.ocr == ocr &&
|
||||
other.people == people &&
|
||||
other.location == location &&
|
||||
other.camera == camera &&
|
||||
|
|
@ -270,6 +276,7 @@ class SearchFilter {
|
|||
filename.hashCode ^
|
||||
description.hashCode ^
|
||||
language.hashCode ^
|
||||
ocr.hashCode ^
|
||||
people.hashCode ^
|
||||
location.hashCode ^
|
||||
camera.hashCode ^
|
||||
|
|
|
|||
|
|
@ -399,6 +399,22 @@ class SearchPage extends HookConsumerWidget {
|
|||
case TextSearchType.description:
|
||||
filter.value = filter.value.copyWith(filename: '', context: '', description: value);
|
||||
break;
|
||||
case TextSearchType.ocr:
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: '',
|
||||
context: '',
|
||||
description: '',
|
||||
ocr: value,
|
||||
);
|
||||
break;
|
||||
case TextSearchType.ocr:
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: '',
|
||||
context: '',
|
||||
description: '',
|
||||
ocr: value,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
search();
|
||||
|
|
@ -408,6 +424,7 @@ class SearchPage extends HookConsumerWidget {
|
|||
TextSearchType.context => Icons.image_search_rounded,
|
||||
TextSearchType.filename => Icons.abc_rounded,
|
||||
TextSearchType.description => Icons.text_snippet_outlined,
|
||||
TextSearchType.ocr => Icons.document_scanner_outlined
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
|
|
@ -493,6 +510,26 @@ class SearchPage extends HookConsumerWidget {
|
|||
searchHintText.value = 'search_by_description_example'.tr();
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.document_scanner_outlined),
|
||||
title: Text(
|
||||
'search_filter_ocr'.tr(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textSearchType.value == TextSearchType.ocr
|
||||
? context.colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
selectedColor: context.colorScheme.primary,
|
||||
selected: textSearchType.value == TextSearchType.ocr,
|
||||
),
|
||||
onPressed: () {
|
||||
textSearchType.value = TextSearchType.ocr;
|
||||
searchHintText.value = 'search_by_ocr_example'.tr();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
|
|
@ -194,6 +194,7 @@ Class | Method | HTTP request | Description
|
|||
*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics |
|
||||
*SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata |
|
||||
*SearchApi* | [**searchLargeAssets**](doc//SearchApi.md#searchlargeassets) | **POST** /search/large-assets |
|
||||
*SearchApi* | [**searchOcr**](doc//SearchApi.md#searchocr) | **POST** /search/ocr |
|
||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
|
||||
|
|
@ -415,6 +416,8 @@ Class | Method | HTTP request | Description
|
|||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||
- [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md)
|
||||
- [OcrConfig](doc//OcrConfig.md)
|
||||
- [OcrSearchDto](doc//OcrSearchDto.md)
|
||||
- [OnThisDayDto](doc//OnThisDayDto.md)
|
||||
- [OnboardingDto](doc//OnboardingDto.md)
|
||||
- [OnboardingResponseDto](doc//OnboardingResponseDto.md)
|
||||
|
|
|
|||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
|
|
@ -187,6 +187,8 @@ part 'model/o_auth_authorize_response_dto.dart';
|
|||
part 'model/o_auth_callback_dto.dart';
|
||||
part 'model/o_auth_config_dto.dart';
|
||||
part 'model/o_auth_token_endpoint_auth_method.dart';
|
||||
part 'model/ocr_config.dart';
|
||||
part 'model/ocr_search_dto.dart';
|
||||
part 'model/on_this_day_dto.dart';
|
||||
part 'model/onboarding_dto.dart';
|
||||
part 'model/onboarding_response_dto.dart';
|
||||
|
|
|
|||
47
mobile/openapi/lib/api/search_api.dart
generated
47
mobile/openapi/lib/api/search_api.dart
generated
|
|
@ -577,6 +577,53 @@ class SearchApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /search/ocr' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [OcrSearchDto] ocrSearchDto (required):
|
||||
Future<Response> searchOcrWithHttpInfo(OcrSearchDto ocrSearchDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/search/ocr';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = ocrSearchDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [OcrSearchDto] ocrSearchDto (required):
|
||||
Future<SearchResponseDto?> searchOcr(OcrSearchDto ocrSearchDto,) async {
|
||||
final response = await searchOcrWithHttpInfo(ocrSearchDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// This endpoint requires the `person.read` permission.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
|
|
|
|||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
|
|
@ -428,6 +428,10 @@ class ApiClient {
|
|||
return OAuthConfigDto.fromJson(value);
|
||||
case 'OAuthTokenEndpointAuthMethod':
|
||||
return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value);
|
||||
case 'OcrConfig':
|
||||
return OcrConfig.fromJson(value);
|
||||
case 'OcrSearchDto':
|
||||
return OcrSearchDto.fromJson(value);
|
||||
case 'OnThisDayDto':
|
||||
return OnThisDayDto.fromJson(value);
|
||||
case 'OnboardingDto':
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class AllJobStatusResponseDto {
|
|||
required this.metadataExtraction,
|
||||
required this.migration,
|
||||
required this.notifications,
|
||||
required this.ocr,
|
||||
required this.search,
|
||||
required this.sidecar,
|
||||
required this.smartSearch,
|
||||
|
|
@ -48,6 +49,8 @@ class AllJobStatusResponseDto {
|
|||
|
||||
JobStatusDto notifications;
|
||||
|
||||
JobStatusDto ocr;
|
||||
|
||||
JobStatusDto search;
|
||||
|
||||
JobStatusDto sidecar;
|
||||
|
|
@ -71,6 +74,7 @@ class AllJobStatusResponseDto {
|
|||
other.metadataExtraction == metadataExtraction &&
|
||||
other.migration == migration &&
|
||||
other.notifications == notifications &&
|
||||
other.ocr == ocr &&
|
||||
other.search == search &&
|
||||
other.sidecar == sidecar &&
|
||||
other.smartSearch == smartSearch &&
|
||||
|
|
@ -90,6 +94,7 @@ class AllJobStatusResponseDto {
|
|||
(metadataExtraction.hashCode) +
|
||||
(migration.hashCode) +
|
||||
(notifications.hashCode) +
|
||||
(ocr.hashCode) +
|
||||
(search.hashCode) +
|
||||
(sidecar.hashCode) +
|
||||
(smartSearch.hashCode) +
|
||||
|
|
@ -98,7 +103,7 @@ class AllJobStatusResponseDto {
|
|||
(videoConversion.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
|
@ -111,6 +116,7 @@ class AllJobStatusResponseDto {
|
|||
json[r'metadataExtraction'] = this.metadataExtraction;
|
||||
json[r'migration'] = this.migration;
|
||||
json[r'notifications'] = this.notifications;
|
||||
json[r'ocr'] = this.ocr;
|
||||
json[r'search'] = this.search;
|
||||
json[r'sidecar'] = this.sidecar;
|
||||
json[r'smartSearch'] = this.smartSearch;
|
||||
|
|
@ -138,6 +144,7 @@ class AllJobStatusResponseDto {
|
|||
metadataExtraction: JobStatusDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: JobStatusDto.fromJson(json[r'migration'])!,
|
||||
notifications: JobStatusDto.fromJson(json[r'notifications'])!,
|
||||
ocr: JobStatusDto.fromJson(json[r'ocr'])!,
|
||||
search: JobStatusDto.fromJson(json[r'search'])!,
|
||||
sidecar: JobStatusDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: JobStatusDto.fromJson(json[r'smartSearch'])!,
|
||||
|
|
@ -200,6 +207,7 @@ class AllJobStatusResponseDto {
|
|||
'metadataExtraction',
|
||||
'migration',
|
||||
'notifications',
|
||||
'ocr',
|
||||
'search',
|
||||
'sidecar',
|
||||
'smartSearch',
|
||||
|
|
|
|||
3
mobile/openapi/lib/model/job_name.dart
generated
3
mobile/openapi/lib/model/job_name.dart
generated
|
|
@ -38,6 +38,7 @@ class JobName {
|
|||
static const library_ = JobName._(r'library');
|
||||
static const notifications = JobName._(r'notifications');
|
||||
static const backupDatabase = JobName._(r'backupDatabase');
|
||||
static const ocr = JobName._(r'ocr');
|
||||
|
||||
/// List of all possible values in this [enum][JobName].
|
||||
static const values = <JobName>[
|
||||
|
|
@ -56,6 +57,7 @@ class JobName {
|
|||
library_,
|
||||
notifications,
|
||||
backupDatabase,
|
||||
ocr,
|
||||
];
|
||||
|
||||
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
|
||||
|
|
@ -109,6 +111,7 @@ class JobNameTypeTransformer {
|
|||
case r'library': return JobName.library_;
|
||||
case r'notifications': return JobName.notifications;
|
||||
case r'backupDatabase': return JobName.backupDatabase;
|
||||
case r'ocr': return JobName.ocr;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
|
|
|||
117
mobile/openapi/lib/model/ocr_config.dart
generated
Normal file
117
mobile/openapi/lib/model/ocr_config.dart
generated
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
//
|
||||
// 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 OcrConfig {
|
||||
/// Returns a new [OcrConfig] instance.
|
||||
OcrConfig({
|
||||
required this.enabled,
|
||||
required this.minScore,
|
||||
required this.modelName,
|
||||
});
|
||||
|
||||
bool enabled;
|
||||
|
||||
/// Minimum value: 0.1
|
||||
/// Maximum value: 1
|
||||
double minScore;
|
||||
|
||||
String modelName;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is OcrConfig &&
|
||||
other.enabled == enabled &&
|
||||
other.minScore == minScore &&
|
||||
other.modelName == modelName;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(enabled.hashCode) +
|
||||
(minScore.hashCode) +
|
||||
(modelName.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'OcrConfig[enabled=$enabled, minScore=$minScore, modelName=$modelName]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'minScore'] = this.minScore;
|
||||
json[r'modelName'] = this.modelName;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [OcrConfig] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static OcrConfig? fromJson(dynamic value) {
|
||||
upgradeDto(value, "OcrConfig");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return OcrConfig(
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
minScore: (mapValueOfType<num>(json, r'minScore')!).toDouble(),
|
||||
modelName: mapValueOfType<String>(json, r'modelName')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<OcrConfig> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <OcrConfig>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = OcrConfig.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, OcrConfig> mapFromJson(dynamic json) {
|
||||
final map = <String, OcrConfig>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = OcrConfig.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of OcrConfig-objects as value to a dart map
|
||||
static Map<String, List<OcrConfig>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<OcrConfig>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = OcrConfig.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'enabled',
|
||||
'minScore',
|
||||
'modelName',
|
||||
};
|
||||
}
|
||||
|
||||
522
mobile/openapi/lib/model/ocr_search_dto.dart
generated
Normal file
522
mobile/openapi/lib/model/ocr_search_dto.dart
generated
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
//
|
||||
// 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 OcrSearchDto {
|
||||
/// Returns a new [OcrSearchDto] instance.
|
||||
OcrSearchDto({
|
||||
this.albumIds = const [],
|
||||
this.city,
|
||||
this.country,
|
||||
this.createdAfter,
|
||||
this.createdBefore,
|
||||
this.deviceId,
|
||||
this.isEncoded,
|
||||
this.isFavorite,
|
||||
this.isMotion,
|
||||
this.isNotInAlbum,
|
||||
this.isOffline,
|
||||
this.lensModel,
|
||||
this.libraryId,
|
||||
this.make,
|
||||
this.model,
|
||||
required this.ocr,
|
||||
this.page,
|
||||
this.personIds = const [],
|
||||
this.rating,
|
||||
this.state,
|
||||
this.tagIds = const [],
|
||||
this.takenAfter,
|
||||
this.takenBefore,
|
||||
this.trashedAfter,
|
||||
this.trashedBefore,
|
||||
this.type,
|
||||
this.updatedAfter,
|
||||
this.updatedBefore,
|
||||
this.visibility,
|
||||
});
|
||||
|
||||
List<String> albumIds;
|
||||
|
||||
String? city;
|
||||
|
||||
String? country;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? createdAfter;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? createdBefore;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? deviceId;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isEncoded;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isFavorite;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isMotion;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isNotInAlbum;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isOffline;
|
||||
|
||||
String? lensModel;
|
||||
|
||||
String? libraryId;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? make;
|
||||
|
||||
String? model;
|
||||
|
||||
String ocr;
|
||||
|
||||
/// Minimum value: 1
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? page;
|
||||
|
||||
List<String> personIds;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? rating;
|
||||
|
||||
String? state;
|
||||
|
||||
List<String>? tagIds;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? takenAfter;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? takenBefore;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? trashedAfter;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? trashedBefore;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
AssetTypeEnum? type;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? updatedAfter;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
DateTime? updatedBefore;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
AssetVisibility? visibility;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is OcrSearchDto &&
|
||||
_deepEquality.equals(other.albumIds, albumIds) &&
|
||||
other.city == city &&
|
||||
other.country == country &&
|
||||
other.createdAfter == createdAfter &&
|
||||
other.createdBefore == createdBefore &&
|
||||
other.deviceId == deviceId &&
|
||||
other.isEncoded == isEncoded &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isMotion == isMotion &&
|
||||
other.isNotInAlbum == isNotInAlbum &&
|
||||
other.isOffline == isOffline &&
|
||||
other.lensModel == lensModel &&
|
||||
other.libraryId == libraryId &&
|
||||
other.make == make &&
|
||||
other.model == model &&
|
||||
other.ocr == ocr &&
|
||||
other.page == page &&
|
||||
_deepEquality.equals(other.personIds, personIds) &&
|
||||
other.rating == rating &&
|
||||
other.state == state &&
|
||||
_deepEquality.equals(other.tagIds, tagIds) &&
|
||||
other.takenAfter == takenAfter &&
|
||||
other.takenBefore == takenBefore &&
|
||||
other.trashedAfter == trashedAfter &&
|
||||
other.trashedBefore == trashedBefore &&
|
||||
other.type == type &&
|
||||
other.updatedAfter == updatedAfter &&
|
||||
other.updatedBefore == updatedBefore &&
|
||||
other.visibility == visibility;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumIds.hashCode) +
|
||||
(city == null ? 0 : city!.hashCode) +
|
||||
(country == null ? 0 : country!.hashCode) +
|
||||
(createdAfter == null ? 0 : createdAfter!.hashCode) +
|
||||
(createdBefore == null ? 0 : createdBefore!.hashCode) +
|
||||
(deviceId == null ? 0 : deviceId!.hashCode) +
|
||||
(isEncoded == null ? 0 : isEncoded!.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isMotion == null ? 0 : isMotion!.hashCode) +
|
||||
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
|
||||
(isOffline == null ? 0 : isOffline!.hashCode) +
|
||||
(lensModel == null ? 0 : lensModel!.hashCode) +
|
||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||
(make == null ? 0 : make!.hashCode) +
|
||||
(model == null ? 0 : model!.hashCode) +
|
||||
(ocr.hashCode) +
|
||||
(page == null ? 0 : page!.hashCode) +
|
||||
(personIds.hashCode) +
|
||||
(rating == null ? 0 : rating!.hashCode) +
|
||||
(state == null ? 0 : state!.hashCode) +
|
||||
(tagIds == null ? 0 : tagIds!.hashCode) +
|
||||
(takenAfter == null ? 0 : takenAfter!.hashCode) +
|
||||
(takenBefore == null ? 0 : takenBefore!.hashCode) +
|
||||
(trashedAfter == null ? 0 : trashedAfter!.hashCode) +
|
||||
(trashedBefore == null ? 0 : trashedBefore!.hashCode) +
|
||||
(type == null ? 0 : type!.hashCode) +
|
||||
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
|
||||
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
|
||||
(visibility == null ? 0 : visibility!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'OcrSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'albumIds'] = this.albumIds;
|
||||
if (this.city != null) {
|
||||
json[r'city'] = this.city;
|
||||
} else {
|
||||
// json[r'city'] = null;
|
||||
}
|
||||
if (this.country != null) {
|
||||
json[r'country'] = this.country;
|
||||
} else {
|
||||
// json[r'country'] = null;
|
||||
}
|
||||
if (this.createdAfter != null) {
|
||||
json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'createdAfter'] = null;
|
||||
}
|
||||
if (this.createdBefore != null) {
|
||||
json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'createdBefore'] = null;
|
||||
}
|
||||
if (this.deviceId != null) {
|
||||
json[r'deviceId'] = this.deviceId;
|
||||
} else {
|
||||
// json[r'deviceId'] = null;
|
||||
}
|
||||
if (this.isEncoded != null) {
|
||||
json[r'isEncoded'] = this.isEncoded;
|
||||
} else {
|
||||
// json[r'isEncoded'] = null;
|
||||
}
|
||||
if (this.isFavorite != null) {
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
} else {
|
||||
// json[r'isFavorite'] = null;
|
||||
}
|
||||
if (this.isMotion != null) {
|
||||
json[r'isMotion'] = this.isMotion;
|
||||
} else {
|
||||
// json[r'isMotion'] = null;
|
||||
}
|
||||
if (this.isNotInAlbum != null) {
|
||||
json[r'isNotInAlbum'] = this.isNotInAlbum;
|
||||
} else {
|
||||
// json[r'isNotInAlbum'] = null;
|
||||
}
|
||||
if (this.isOffline != null) {
|
||||
json[r'isOffline'] = this.isOffline;
|
||||
} else {
|
||||
// json[r'isOffline'] = null;
|
||||
}
|
||||
if (this.lensModel != null) {
|
||||
json[r'lensModel'] = this.lensModel;
|
||||
} else {
|
||||
// json[r'lensModel'] = null;
|
||||
}
|
||||
if (this.libraryId != null) {
|
||||
json[r'libraryId'] = this.libraryId;
|
||||
} else {
|
||||
// json[r'libraryId'] = null;
|
||||
}
|
||||
if (this.make != null) {
|
||||
json[r'make'] = this.make;
|
||||
} else {
|
||||
// json[r'make'] = null;
|
||||
}
|
||||
if (this.model != null) {
|
||||
json[r'model'] = this.model;
|
||||
} else {
|
||||
// json[r'model'] = null;
|
||||
}
|
||||
json[r'ocr'] = this.ocr;
|
||||
if (this.page != null) {
|
||||
json[r'page'] = this.page;
|
||||
} else {
|
||||
// json[r'page'] = null;
|
||||
}
|
||||
json[r'personIds'] = this.personIds;
|
||||
if (this.rating != null) {
|
||||
json[r'rating'] = this.rating;
|
||||
} else {
|
||||
// json[r'rating'] = null;
|
||||
}
|
||||
if (this.state != null) {
|
||||
json[r'state'] = this.state;
|
||||
} else {
|
||||
// json[r'state'] = null;
|
||||
}
|
||||
if (this.tagIds != null) {
|
||||
json[r'tagIds'] = this.tagIds;
|
||||
} else {
|
||||
// json[r'tagIds'] = null;
|
||||
}
|
||||
if (this.takenAfter != null) {
|
||||
json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'takenAfter'] = null;
|
||||
}
|
||||
if (this.takenBefore != null) {
|
||||
json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'takenBefore'] = null;
|
||||
}
|
||||
if (this.trashedAfter != null) {
|
||||
json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'trashedAfter'] = null;
|
||||
}
|
||||
if (this.trashedBefore != null) {
|
||||
json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'trashedBefore'] = null;
|
||||
}
|
||||
if (this.type != null) {
|
||||
json[r'type'] = this.type;
|
||||
} else {
|
||||
// json[r'type'] = null;
|
||||
}
|
||||
if (this.updatedAfter != null) {
|
||||
json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'updatedAfter'] = null;
|
||||
}
|
||||
if (this.updatedBefore != null) {
|
||||
json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'updatedBefore'] = null;
|
||||
}
|
||||
if (this.visibility != null) {
|
||||
json[r'visibility'] = this.visibility;
|
||||
} else {
|
||||
// json[r'visibility'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [OcrSearchDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static OcrSearchDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "OcrSearchDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return OcrSearchDto(
|
||||
albumIds: json[r'albumIds'] is Iterable
|
||||
? (json[r'albumIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
city: mapValueOfType<String>(json, r'city'),
|
||||
country: mapValueOfType<String>(json, r'country'),
|
||||
createdAfter: mapDateTime(json, r'createdAfter', r''),
|
||||
createdBefore: mapDateTime(json, r'createdBefore', r''),
|
||||
deviceId: mapValueOfType<String>(json, r'deviceId'),
|
||||
isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isMotion: mapValueOfType<bool>(json, r'isMotion'),
|
||||
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
|
||||
isOffline: mapValueOfType<bool>(json, r'isOffline'),
|
||||
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
make: mapValueOfType<String>(json, r'make'),
|
||||
model: mapValueOfType<String>(json, r'model'),
|
||||
ocr: mapValueOfType<String>(json, r'ocr')!,
|
||||
page: num.parse('${json[r'page']}'),
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
takenAfter: mapDateTime(json, r'takenAfter', r''),
|
||||
takenBefore: mapDateTime(json, r'takenBefore', r''),
|
||||
trashedAfter: mapDateTime(json, r'trashedAfter', r''),
|
||||
trashedBefore: mapDateTime(json, r'trashedBefore', r''),
|
||||
type: AssetTypeEnum.fromJson(json[r'type']),
|
||||
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
|
||||
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<OcrSearchDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <OcrSearchDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = OcrSearchDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, OcrSearchDto> mapFromJson(dynamic json) {
|
||||
final map = <String, OcrSearchDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = OcrSearchDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of OcrSearchDto-objects as value to a dart map
|
||||
static Map<String, List<OcrSearchDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<OcrSearchDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = OcrSearchDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'ocr',
|
||||
};
|
||||
}
|
||||
|
||||
10
mobile/openapi/lib/model/server_features_dto.dart
generated
10
mobile/openapi/lib/model/server_features_dto.dart
generated
|
|
@ -21,6 +21,7 @@ class ServerFeaturesDto {
|
|||
required this.map,
|
||||
required this.oauth,
|
||||
required this.oauthAutoLaunch,
|
||||
required this.ocr,
|
||||
required this.passwordLogin,
|
||||
required this.reverseGeocoding,
|
||||
required this.search,
|
||||
|
|
@ -45,6 +46,8 @@ class ServerFeaturesDto {
|
|||
|
||||
bool oauthAutoLaunch;
|
||||
|
||||
bool ocr;
|
||||
|
||||
bool passwordLogin;
|
||||
|
||||
bool reverseGeocoding;
|
||||
|
|
@ -67,6 +70,7 @@ class ServerFeaturesDto {
|
|||
other.map == map &&
|
||||
other.oauth == oauth &&
|
||||
other.oauthAutoLaunch == oauthAutoLaunch &&
|
||||
other.ocr == ocr &&
|
||||
other.passwordLogin == passwordLogin &&
|
||||
other.reverseGeocoding == reverseGeocoding &&
|
||||
other.search == search &&
|
||||
|
|
@ -85,6 +89,7 @@ class ServerFeaturesDto {
|
|||
(map.hashCode) +
|
||||
(oauth.hashCode) +
|
||||
(oauthAutoLaunch.hashCode) +
|
||||
(ocr.hashCode) +
|
||||
(passwordLogin.hashCode) +
|
||||
(reverseGeocoding.hashCode) +
|
||||
(search.hashCode) +
|
||||
|
|
@ -93,7 +98,7 @@ class ServerFeaturesDto {
|
|||
(trash.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
|
||||
String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, ocr=$ocr, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
|
@ -105,6 +110,7 @@ class ServerFeaturesDto {
|
|||
json[r'map'] = this.map;
|
||||
json[r'oauth'] = this.oauth;
|
||||
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
|
||||
json[r'ocr'] = this.ocr;
|
||||
json[r'passwordLogin'] = this.passwordLogin;
|
||||
json[r'reverseGeocoding'] = this.reverseGeocoding;
|
||||
json[r'search'] = this.search;
|
||||
|
|
@ -131,6 +137,7 @@ class ServerFeaturesDto {
|
|||
map: mapValueOfType<bool>(json, r'map')!,
|
||||
oauth: mapValueOfType<bool>(json, r'oauth')!,
|
||||
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
|
||||
ocr: mapValueOfType<bool>(json, r'ocr')!,
|
||||
passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
|
||||
reverseGeocoding: mapValueOfType<bool>(json, r'reverseGeocoding')!,
|
||||
search: mapValueOfType<bool>(json, r'search')!,
|
||||
|
|
@ -192,6 +199,7 @@ class ServerFeaturesDto {
|
|||
'map',
|
||||
'oauth',
|
||||
'oauthAutoLaunch',
|
||||
'ocr',
|
||||
'passwordLogin',
|
||||
'reverseGeocoding',
|
||||
'search',
|
||||
|
|
|
|||
10
mobile/openapi/lib/model/system_config_job_dto.dart
generated
10
mobile/openapi/lib/model/system_config_job_dto.dart
generated
|
|
@ -19,6 +19,7 @@ class SystemConfigJobDto {
|
|||
required this.metadataExtraction,
|
||||
required this.migration,
|
||||
required this.notifications,
|
||||
required this.ocr,
|
||||
required this.search,
|
||||
required this.sidecar,
|
||||
required this.smartSearch,
|
||||
|
|
@ -38,6 +39,8 @@ class SystemConfigJobDto {
|
|||
|
||||
JobSettingsDto notifications;
|
||||
|
||||
JobSettingsDto ocr;
|
||||
|
||||
JobSettingsDto search;
|
||||
|
||||
JobSettingsDto sidecar;
|
||||
|
|
@ -56,6 +59,7 @@ class SystemConfigJobDto {
|
|||
other.metadataExtraction == metadataExtraction &&
|
||||
other.migration == migration &&
|
||||
other.notifications == notifications &&
|
||||
other.ocr == ocr &&
|
||||
other.search == search &&
|
||||
other.sidecar == sidecar &&
|
||||
other.smartSearch == smartSearch &&
|
||||
|
|
@ -71,6 +75,7 @@ class SystemConfigJobDto {
|
|||
(metadataExtraction.hashCode) +
|
||||
(migration.hashCode) +
|
||||
(notifications.hashCode) +
|
||||
(ocr.hashCode) +
|
||||
(search.hashCode) +
|
||||
(sidecar.hashCode) +
|
||||
(smartSearch.hashCode) +
|
||||
|
|
@ -78,7 +83,7 @@ class SystemConfigJobDto {
|
|||
(videoConversion.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
|
@ -88,6 +93,7 @@ class SystemConfigJobDto {
|
|||
json[r'metadataExtraction'] = this.metadataExtraction;
|
||||
json[r'migration'] = this.migration;
|
||||
json[r'notifications'] = this.notifications;
|
||||
json[r'ocr'] = this.ocr;
|
||||
json[r'search'] = this.search;
|
||||
json[r'sidecar'] = this.sidecar;
|
||||
json[r'smartSearch'] = this.smartSearch;
|
||||
|
|
@ -111,6 +117,7 @@ class SystemConfigJobDto {
|
|||
metadataExtraction: JobSettingsDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: JobSettingsDto.fromJson(json[r'migration'])!,
|
||||
notifications: JobSettingsDto.fromJson(json[r'notifications'])!,
|
||||
ocr: JobSettingsDto.fromJson(json[r'ocr'])!,
|
||||
search: JobSettingsDto.fromJson(json[r'search'])!,
|
||||
sidecar: JobSettingsDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: JobSettingsDto.fromJson(json[r'smartSearch'])!,
|
||||
|
|
@ -169,6 +176,7 @@ class SystemConfigJobDto {
|
|||
'metadataExtraction',
|
||||
'migration',
|
||||
'notifications',
|
||||
'ocr',
|
||||
'search',
|
||||
'sidecar',
|
||||
'smartSearch',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class SystemConfigMachineLearningDto {
|
|||
required this.duplicateDetection,
|
||||
required this.enabled,
|
||||
required this.facialRecognition,
|
||||
required this.ocr,
|
||||
this.url,
|
||||
this.urls = const [],
|
||||
});
|
||||
|
|
@ -29,6 +30,8 @@ class SystemConfigMachineLearningDto {
|
|||
|
||||
FacialRecognitionConfig facialRecognition;
|
||||
|
||||
OcrConfig ocr;
|
||||
|
||||
/// This property was deprecated in v1.122.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
|
|
@ -46,6 +49,7 @@ class SystemConfigMachineLearningDto {
|
|||
other.duplicateDetection == duplicateDetection &&
|
||||
other.enabled == enabled &&
|
||||
other.facialRecognition == facialRecognition &&
|
||||
other.ocr == ocr &&
|
||||
other.url == url &&
|
||||
_deepEquality.equals(other.urls, urls);
|
||||
|
||||
|
|
@ -56,11 +60,12 @@ class SystemConfigMachineLearningDto {
|
|||
(duplicateDetection.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(facialRecognition.hashCode) +
|
||||
(ocr.hashCode) +
|
||||
(url == null ? 0 : url!.hashCode) +
|
||||
(urls.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, url=$url, urls=$urls]';
|
||||
String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, ocr=$ocr, url=$url, urls=$urls]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
|
@ -68,6 +73,7 @@ class SystemConfigMachineLearningDto {
|
|||
json[r'duplicateDetection'] = this.duplicateDetection;
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'facialRecognition'] = this.facialRecognition;
|
||||
json[r'ocr'] = this.ocr;
|
||||
if (this.url != null) {
|
||||
json[r'url'] = this.url;
|
||||
} else {
|
||||
|
|
@ -90,6 +96,7 @@ class SystemConfigMachineLearningDto {
|
|||
duplicateDetection: DuplicateDetectionConfig.fromJson(json[r'duplicateDetection'])!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
facialRecognition: FacialRecognitionConfig.fromJson(json[r'facialRecognition'])!,
|
||||
ocr: OcrConfig.fromJson(json[r'ocr'])!,
|
||||
url: mapValueOfType<String>(json, r'url'),
|
||||
urls: json[r'urls'] is Iterable
|
||||
? (json[r'urls'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
|
@ -145,6 +152,7 @@ class SystemConfigMachineLearningDto {
|
|||
'duplicateDetection',
|
||||
'enabled',
|
||||
'facialRecognition',
|
||||
'ocr',
|
||||
'urls',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6151,6 +6151,48 @@
|
|||
"description": "This endpoint requires the `asset.read` permission."
|
||||
}
|
||||
},
|
||||
"/search/ocr": {
|
||||
"post": {
|
||||
"operationId": "searchOcr",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OcrSearchDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/person": {
|
||||
"get": {
|
||||
"operationId": "searchPerson",
|
||||
|
|
@ -10325,6 +10367,9 @@
|
|||
"notifications": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"search": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
|
|
@ -10354,6 +10399,7 @@
|
|||
"metadataExtraction",
|
||||
"migration",
|
||||
"notifications",
|
||||
"ocr",
|
||||
"search",
|
||||
"sidecar",
|
||||
"smartSearch",
|
||||
|
|
@ -12006,7 +12052,8 @@
|
|||
"sidecar",
|
||||
"library",
|
||||
"notifications",
|
||||
"backupDatabase"
|
||||
"backupDatabase",
|
||||
"ocr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -12866,6 +12913,162 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"OcrConfig": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"minScore": {
|
||||
"format": "double",
|
||||
"maximum": 1,
|
||||
"minimum": 0.1,
|
||||
"type": "number"
|
||||
},
|
||||
"modelName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"minScore",
|
||||
"modelName"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OcrSearchDto": {
|
||||
"properties": {
|
||||
"albumIds": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"city": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"createdAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"createdBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isEncoded": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isMotion": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNotInAlbum": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOffline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lensModel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"libraryId": {
|
||||
"format": "uuid",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"make": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ocr": {
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"tagIds": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "array"
|
||||
},
|
||||
"takenAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"takenBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"trashedAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"trashedBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||
}
|
||||
]
|
||||
},
|
||||
"updatedAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ocr"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OnThisDayDto": {
|
||||
"properties": {
|
||||
"year": {
|
||||
|
|
@ -14002,6 +14205,9 @@
|
|||
"oauthAutoLaunch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ocr": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"passwordLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -14030,6 +14236,7 @@
|
|||
"map",
|
||||
"oauth",
|
||||
"oauthAutoLaunch",
|
||||
"ocr",
|
||||
"passwordLogin",
|
||||
"reverseGeocoding",
|
||||
"search",
|
||||
|
|
@ -16281,6 +16488,9 @@
|
|||
"notifications": {
|
||||
"$ref": "#/components/schemas/JobSettingsDto"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/JobSettingsDto"
|
||||
},
|
||||
"search": {
|
||||
"$ref": "#/components/schemas/JobSettingsDto"
|
||||
},
|
||||
|
|
@ -16304,6 +16514,7 @@
|
|||
"metadataExtraction",
|
||||
"migration",
|
||||
"notifications",
|
||||
"ocr",
|
||||
"search",
|
||||
"sidecar",
|
||||
"smartSearch",
|
||||
|
|
@ -16386,6 +16597,9 @@
|
|||
"facialRecognition": {
|
||||
"$ref": "#/components/schemas/FacialRecognitionConfig"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/OcrConfig"
|
||||
},
|
||||
"url": {
|
||||
"deprecated": true,
|
||||
"description": "This property was deprecated in v1.122.0",
|
||||
|
|
@ -16406,6 +16620,7 @@
|
|||
"duplicateDetection",
|
||||
"enabled",
|
||||
"facialRecognition",
|
||||
"ocr",
|
||||
"urls"
|
||||
],
|
||||
"type": "object"
|
||||
|
|
|
|||
|
|
@ -678,6 +678,7 @@ export type AllJobStatusResponseDto = {
|
|||
metadataExtraction: JobStatusDto;
|
||||
migration: JobStatusDto;
|
||||
notifications: JobStatusDto;
|
||||
ocr: JobStatusDto;
|
||||
search: JobStatusDto;
|
||||
sidecar: JobStatusDto;
|
||||
smartSearch: JobStatusDto;
|
||||
|
|
@ -957,6 +958,37 @@ export type SearchResponseDto = {
|
|||
albums: SearchAlbumResponseDto;
|
||||
assets: SearchAssetResponseDto;
|
||||
};
|
||||
export type OcrSearchDto = {
|
||||
albumIds?: string[];
|
||||
city?: string | null;
|
||||
country?: string | null;
|
||||
createdAfter?: string;
|
||||
createdBefore?: string;
|
||||
deviceId?: string;
|
||||
isEncoded?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
isOffline?: boolean;
|
||||
lensModel?: string | null;
|
||||
libraryId?: string | null;
|
||||
make?: string;
|
||||
model?: string | null;
|
||||
ocr: string;
|
||||
page?: number;
|
||||
personIds?: string[];
|
||||
rating?: number;
|
||||
state?: string | null;
|
||||
tagIds?: string[] | null;
|
||||
takenAfter?: string;
|
||||
takenBefore?: string;
|
||||
trashedAfter?: string;
|
||||
trashedBefore?: string;
|
||||
"type"?: AssetTypeEnum;
|
||||
updatedAfter?: string;
|
||||
updatedBefore?: string;
|
||||
visibility?: AssetVisibility;
|
||||
};
|
||||
export type PlacesResponseDto = {
|
||||
admin1name?: string;
|
||||
admin2name?: string;
|
||||
|
|
@ -1117,6 +1149,7 @@ export type ServerFeaturesDto = {
|
|||
map: boolean;
|
||||
oauth: boolean;
|
||||
oauthAutoLaunch: boolean;
|
||||
ocr: boolean;
|
||||
passwordLogin: boolean;
|
||||
reverseGeocoding: boolean;
|
||||
search: boolean;
|
||||
|
|
@ -1362,6 +1395,7 @@ export type SystemConfigJobDto = {
|
|||
metadataExtraction: JobSettingsDto;
|
||||
migration: JobSettingsDto;
|
||||
notifications: JobSettingsDto;
|
||||
ocr: JobSettingsDto;
|
||||
search: JobSettingsDto;
|
||||
sidecar: JobSettingsDto;
|
||||
smartSearch: JobSettingsDto;
|
||||
|
|
@ -1398,11 +1432,17 @@ export type FacialRecognitionConfig = {
|
|||
minScore: number;
|
||||
modelName: string;
|
||||
};
|
||||
export type OcrConfig = {
|
||||
enabled: boolean;
|
||||
minScore: number;
|
||||
modelName: string;
|
||||
};
|
||||
export type SystemConfigMachineLearningDto = {
|
||||
clip: ClipConfig;
|
||||
duplicateDetection: DuplicateDetectionConfig;
|
||||
enabled: boolean;
|
||||
facialRecognition: FacialRecognitionConfig;
|
||||
ocr: OcrConfig;
|
||||
/** This property was deprecated in v1.122.0 */
|
||||
url?: string;
|
||||
urls: string[];
|
||||
|
|
@ -3457,6 +3497,18 @@ export function searchAssets({ metadataSearchDto }: {
|
|||
body: metadataSearchDto
|
||||
})));
|
||||
}
|
||||
export function searchOcr({ ocrSearchDto }: {
|
||||
ocrSearchDto: OcrSearchDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: SearchResponseDto;
|
||||
}>("/search/ocr", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: ocrSearchDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* This endpoint requires the `person.read` permission.
|
||||
*/
|
||||
|
|
@ -4862,7 +4914,8 @@ export enum JobName {
|
|||
Sidecar = "sidecar",
|
||||
Library = "library",
|
||||
Notifications = "notifications",
|
||||
BackupDatabase = "backupDatabase"
|
||||
BackupDatabase = "backupDatabase",
|
||||
Ocr = "ocr"
|
||||
}
|
||||
export enum JobCommand {
|
||||
Start = "start",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@ export interface SystemConfig {
|
|||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
ocr: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
|
|
@ -219,6 +224,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
[QueueName.ThumbnailGeneration]: { concurrency: 3 },
|
||||
[QueueName.VideoConversion]: { concurrency: 1 },
|
||||
[QueueName.Notification]: { concurrency: 5 },
|
||||
[QueueName.OCR]: { concurrency: 1 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
|
|
@ -242,6 +248,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
maxDistance: 0.5,
|
||||
minFaces: 3,
|
||||
},
|
||||
ocr: {
|
||||
enabled: true,
|
||||
modelName: 'paddle',
|
||||
minScore: 0.9,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
|
|||
import {
|
||||
LargeAssetSearchDto,
|
||||
MetadataSearchDto,
|
||||
OcrSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
SearchExploreResponseDto,
|
||||
|
|
@ -61,6 +62,13 @@ export class SearchController {
|
|||
return this.service.searchSmart(auth, dto);
|
||||
}
|
||||
|
||||
@Post('ocr')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchOcr(@Auth() auth: AuthDto, @Body() dto: OcrSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchOcr(auth, dto);
|
||||
}
|
||||
|
||||
@Get('explore')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
|
||||
|
|
|
|||
|
|
@ -93,4 +93,7 @@ export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto>
|
|||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.BackupDatabase]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.OCR]!: JobStatusDto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,12 @@ export class FacialRecognitionConfig extends ModelConfig {
|
|||
@ApiProperty({ type: 'integer' })
|
||||
minFaces!: number;
|
||||
}
|
||||
|
||||
export class OcrConfig extends ModelConfig {
|
||||
@IsNumber()
|
||||
@Min(0.1)
|
||||
@Max(1)
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
minScore!: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,18 @@ export class SmartSearchDto extends BaseSearchWithResultsDto {
|
|||
page?: number;
|
||||
}
|
||||
|
||||
export class OcrSearchDto extends BaseSearchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
ocr!: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export class SearchPlacesDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ export class ServerFeaturesDto {
|
|||
sidecar!: boolean;
|
||||
search!: boolean;
|
||||
email!: boolean;
|
||||
ocr!: boolean;
|
||||
}
|
||||
|
||||
export interface ReleaseNotification {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig, OcrConfig } from 'src/dtos/model-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
|
|
@ -202,6 +202,12 @@ class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto>
|
|||
@Type(() => JobSettingsDto)
|
||||
[QueueName.FaceDetection]!: JobSettingsDto;
|
||||
|
||||
@ApiProperty({ type: JobSettingsDto })
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@Type(() => JobSettingsDto)
|
||||
[QueueName.OCR]!: JobSettingsDto;
|
||||
|
||||
@ApiProperty({ type: JobSettingsDto })
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
|
@ -286,6 +292,11 @@ class SystemConfigMachineLearningDto {
|
|||
@ValidateNested()
|
||||
@IsObject()
|
||||
facialRecognition!: FacialRecognitionConfig;
|
||||
|
||||
@Type(() => OcrConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
ocr!: OcrConfig;
|
||||
}
|
||||
|
||||
enum MapTheme {
|
||||
|
|
|
|||
5
server/src/entities/ocr.entity.ts
Normal file
5
server/src/entities/ocr.entity.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class OcrEntity {
|
||||
id!: string;
|
||||
assetId!: string;
|
||||
text!: string;
|
||||
}
|
||||
|
|
@ -511,6 +511,7 @@ export enum QueueName {
|
|||
Library = 'library',
|
||||
Notification = 'notifications',
|
||||
BackupDatabase = 'backupDatabase',
|
||||
OCR = 'ocr',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
|
|
@ -583,6 +584,11 @@ export enum JobName {
|
|||
TagCleanup = 'TagCleanup',
|
||||
|
||||
VersionCheck = 'VersionCheck',
|
||||
|
||||
// OCR
|
||||
QUEUE_OCR = 'queue-ocr',
|
||||
OCR = 'ocr',
|
||||
OCR_CLEANUP = 'ocr-cleanup',
|
||||
}
|
||||
|
||||
export enum JobCommand {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpsertOcrAssetJobStatus1743429240851 implements MigrationInterface {
|
||||
name = 'UpsertOcrAssetJobStatus1743429240851';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_job_status
|
||||
ADD COLUMN IF NOT EXISTS "ocrAt" TIMESTAMP WITH TIME ZONE;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_job_status
|
||||
DROP COLUMN IF EXISTS "ocrAt";
|
||||
`);
|
||||
}
|
||||
}
|
||||
22
server/src/migrations/1743429592349-AddOcrTable.ts
Normal file
22
server/src/migrations/1743429592349-AddOcrTable.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddOcrTable1743429592349 implements MigrationInterface {
|
||||
name = 'AddOcrTable1743429592349';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "asset_ocr"
|
||||
(
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"assetId" uuid NOT NULL,
|
||||
"text" text,
|
||||
CONSTRAINT "PK_asset_ocr_id" PRIMARY KEY ("id")
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "asset_ocr" DROP CONSTRAINT "FK_asset_ocr_assetId"`);
|
||||
await queryRunner.query(`DROP TABLE "asset_ocr"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -348,6 +348,23 @@ export class AssetJobRepository {
|
|||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamForOcrJob(force?: boolean) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(['assets.id'])
|
||||
.$if(!force, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id')
|
||||
.where((eb) =>
|
||||
eb.or([eb('asset_job_status.ocrAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
|
||||
)
|
||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
||||
streamForMigrationJob() {
|
||||
return this.db.selectFrom('asset').select(['id']).where('asset.deletedAt', 'is', null).stream();
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ export class AssetRepository {
|
|||
metadataExtractedAt: eb.ref('excluded.metadataExtractedAt'),
|
||||
previewAt: eb.ref('excluded.previewAt'),
|
||||
thumbnailAt: eb.ref('excluded.thumbnailAt'),
|
||||
ocrAt: eb.ref('excluded.ocrAt'),
|
||||
},
|
||||
values[0],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { MetadataRepository } from 'src/repositories/metadata.repository';
|
|||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
|
|
@ -72,6 +73,7 @@ export const repositories = [
|
|||
MoveRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
OcrRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
ProcessRepository,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,9 @@ export class JobRepository {
|
|||
case JobName.FacialRecognitionQueueAll: {
|
||||
return { jobId: JobName.FacialRecognitionQueueAll };
|
||||
}
|
||||
case JobName.QUEUE_OCR: {
|
||||
return { jobId: JobName.QUEUE_OCR };
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface BoundingBox {
|
|||
export enum ModelTask {
|
||||
FACIAL_RECOGNITION = 'facial-recognition',
|
||||
SEARCH = 'clip',
|
||||
OCR = 'ocr',
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
|
|
@ -22,6 +23,7 @@ export enum ModelType {
|
|||
RECOGNITION = 'recognition',
|
||||
TEXTUAL = 'textual',
|
||||
VISUAL = 'visual',
|
||||
OCR = 'ocr',
|
||||
}
|
||||
|
||||
export type ModelPayload = { imagePath: string } | { text: string };
|
||||
|
|
@ -29,7 +31,7 @@ export type ModelPayload = { imagePath: string } | { text: string };
|
|||
type ModelOptions = { modelName: string };
|
||||
|
||||
export type FaceDetectionOptions = ModelOptions & { minScore: number };
|
||||
|
||||
export type OcrOptions = ModelOptions & { minScore: number };
|
||||
type VisualResponse = { imageHeight: number; imageWidth: number };
|
||||
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
|
||||
|
|
@ -37,6 +39,14 @@ export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse
|
|||
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
|
||||
|
||||
export type OCR = {
|
||||
text: string;
|
||||
confidence: number;
|
||||
};
|
||||
|
||||
export type OcrRequest = { [ModelTask.OCR]: { [ModelType.OCR]: ModelOptions & { options: { minScore: number } } } };
|
||||
export type OcrResponse = { [ModelTask.OCR]: OCR } & VisualResponse;
|
||||
|
||||
export type FacialRecognitionRequest = {
|
||||
[ModelTask.FACIAL_RECOGNITION]: {
|
||||
[ModelType.DETECTION]: ModelOptions & { options: { minScore: number } };
|
||||
|
|
@ -52,7 +62,7 @@ export interface Face {
|
|||
|
||||
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
|
||||
export type DetectedFaces = { faces: Face[] } & VisualResponse;
|
||||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
|
||||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest | OcrRequest;
|
||||
export type TextEncodingOptions = ModelOptions & { language?: string };
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -192,4 +202,10 @@ export class MachineLearningRepository {
|
|||
|
||||
return formData;
|
||||
}
|
||||
|
||||
async ocr(urls: string[], imagePath: string, { modelName, minScore }: OcrOptions) {
|
||||
const request = { [ModelTask.OCR]: { [ModelType.OCR]: { modelName, options: { minScore } } } };
|
||||
const response = await this.predict<OcrResponse>(urls, { imagePath }, request);
|
||||
return response[ModelTask.OCR];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
server/src/repositories/ocr.repository.ts
Normal file
74
server/src/repositories/ocr.repository.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { OcrEntity } from 'src/entities/ocr.entity';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class OcrRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getOcrById(id: string): Promise<OcrEntity | null> {
|
||||
return this.db
|
||||
.selectFrom('asset_ocr')
|
||||
.selectAll('asset_ocr')
|
||||
.where('asset_ocr.assetId', '=', id)
|
||||
.executeTakeFirst() as Promise<OcrEntity | null>;
|
||||
}
|
||||
|
||||
async insertOcrData(assetId: string, text: string): Promise<void> {
|
||||
await this.db
|
||||
.insertInto('asset_ocr')
|
||||
.values({ assetId, text })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAllOcr(): Promise<void> {
|
||||
await sql`truncate ${sql.table('asset_ocr')}`.execute(this.db);
|
||||
}
|
||||
|
||||
getAllOcr(options: Partial<OcrEntity> = {}): AsyncIterableIterator<OcrEntity> {
|
||||
return this.db
|
||||
.selectFrom('asset_ocr')
|
||||
.selectAll('asset_ocr')
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_ocr.assetId', '=', options.assetId!))
|
||||
.stream() as AsyncIterableIterator<OcrEntity>;
|
||||
}
|
||||
|
||||
|
||||
@GenerateSql()
|
||||
async getLatestOcrDate(): Promise<string | undefined> {
|
||||
const result = (await this.db
|
||||
.selectFrom('asset_job_status')
|
||||
.select((eb) => sql`${eb.fn.max('asset_job_status.ocrAt')}::text`.as('latestDate'))
|
||||
.executeTakeFirst()) as { latestDate: string } | undefined;
|
||||
|
||||
return result?.latestDate;
|
||||
}
|
||||
|
||||
async updateOcrData(id: string, ocrData: string): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('asset_ocr')
|
||||
.set({ text: ocrData })
|
||||
.where('id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
getOcrWithoutText(): Promise<OcrEntity[]> {
|
||||
return this.db
|
||||
.selectFrom('asset_ocr')
|
||||
.selectAll('asset_ocr')
|
||||
.where('text', 'is', null)
|
||||
.execute() as Promise<OcrEntity[]>;
|
||||
}
|
||||
|
||||
async delete(ocr: OcrEntity[]): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('asset_ocr')
|
||||
.where('id', 'in', ocr.map((o) => o.id))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,11 @@ export interface SearchEmbeddingOptions {
|
|||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchOcrOptions {
|
||||
ocr: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchPeopleOptions {
|
||||
personIds?: string[];
|
||||
}
|
||||
|
|
@ -129,6 +134,8 @@ export type SmartSearchOptions = SearchDateOptions &
|
|||
SearchPeopleOptions &
|
||||
SearchTagOptions;
|
||||
|
||||
export type OcrSearchOptions = SearchDateOptions & SearchOcrOptions;
|
||||
|
||||
export type LargeAssetSearchOptions = AssetSearchOptions & { minFileSize?: number };
|
||||
|
||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
|
|
@ -300,6 +307,35 @@ export class SearchRepository {
|
|||
return this.db.selectFrom('smart_search').selectAll().where('assetId', '=', assetId).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{ page: 1, size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
ocr: DummyValue.STRING,
|
||||
},
|
||||
],
|
||||
})
|
||||
async searchOcr(pagination: SearchPaginationOptions, options: OcrSearchOptions) {
|
||||
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
||||
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
||||
}
|
||||
|
||||
const items = await this.db
|
||||
.selectFrom('asset_ocr')
|
||||
.selectAll()
|
||||
.innerJoin('assets', 'assets.id', 'asset_ocr.assetId')
|
||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||
.where('asset_ocr.text', 'ilike', `%${options.ocr}%`)
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
.execute() as any;
|
||||
|
||||
const hasNextPage = items.length > pagination.size;
|
||||
items.splice(pagination.size);
|
||||
return { items, hasNextPage };
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{
|
||||
|
|
|
|||
13
server/src/schema/tables/asset_ocr.table.ts
Normal file
13
server/src/schema/tables/asset_ocr.table.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Column, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('asset_ocr')
|
||||
export class AssetOcrTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
assetId!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
text!: string;
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import { MetadataRepository } from 'src/repositories/metadata.repository';
|
|||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
|
|
@ -134,6 +135,7 @@ export class BaseService {
|
|||
protected moveRepository: MoveRepository,
|
||||
protected notificationRepository: NotificationRepository,
|
||||
protected oauthRepository: OAuthRepository,
|
||||
protected ocrRepository: OcrRepository,
|
||||
protected partnerRepository: PartnerRepository,
|
||||
protected personRepository: PersonRepository,
|
||||
protected processRepository: ProcessRepository,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { MemoryService } from 'src/services/memory.service';
|
|||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { OcrService } from 'src/services/ocr.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
|
|
@ -64,6 +65,7 @@ export const services = [
|
|||
MetadataService,
|
||||
NotificationService,
|
||||
NotificationAdminService,
|
||||
OcrService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
SearchService,
|
||||
|
|
|
|||
|
|
@ -237,6 +237,14 @@ export class JobService extends BaseService {
|
|||
return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.OCR: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_OCR, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.OCR: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_OCR, data: { force } });
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestException(`Invalid job name: ${name}`);
|
||||
}
|
||||
|
|
@ -300,6 +308,7 @@ export class JobService extends BaseService {
|
|||
|
||||
if (config.nightlyTasks.clusterNewFaces) {
|
||||
jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } });
|
||||
{ name: JobName.QUEUE_OCR, data: { force: false, nightly: true } },
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
|
|
@ -353,6 +362,7 @@ export class JobService extends BaseService {
|
|||
const jobs: JobItem[] = [
|
||||
{ name: JobName.SmartSearch, data: item.data },
|
||||
{ name: JobName.AssetDetectFaces, data: item.data },
|
||||
{ name: JobName.OCR, data: item.data },
|
||||
];
|
||||
|
||||
if (asset.type === AssetType.Video) {
|
||||
|
|
|
|||
0
server/src/services/ocr.service.spec.ts
Normal file
0
server/src/services/ocr.service.spec.ts
Normal file
108
server/src/services/ocr.service.ts
Normal file
108
server/src/services/ocr.service.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import {
|
||||
JobName,
|
||||
JobStatus,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { isOcrEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class OcrService extends BaseService {
|
||||
@OnJob({ name: JobName.OCR_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||
async handleOcrCleanup(): Promise<JobStatus> {
|
||||
const ocr = await this.ocrRepository.getOcrWithoutText();
|
||||
await this.ocrRepository.delete(ocr);
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.QUEUE_OCR, queue: QueueName.OCR })
|
||||
async handleQueueOcr({ force, nightly }: JobOf<JobName.QUEUE_OCR>): Promise<JobStatus> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||
if (!isOcrEnabled(machineLearning)) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
await this.ocrRepository.deleteAllOcr();
|
||||
}
|
||||
|
||||
let jobs: JobItem[] = [];
|
||||
const assets = this.assetJobRepository.streamForOcrJob(force);
|
||||
|
||||
for await (const asset of assets) {
|
||||
jobs.push({ name: JobName.OCR, data: { id: asset.id } });
|
||||
|
||||
if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) {
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
jobs = [];
|
||||
}
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
|
||||
if (force === undefined) {
|
||||
await this.jobRepository.queue({ name: JobName.OCR_CLEANUP });
|
||||
}
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.OCR, queue: QueueName.OCR })
|
||||
async handleOcr({ id }: JobOf<JobName.OCR>): Promise<JobStatus> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||
if (!isOcrEnabled(machineLearning)) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const relations = { files: true };
|
||||
const asset = await this.assetRepository.getById(id, relations);
|
||||
if (!asset) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
if (!asset.files) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
const { previewFile } = getAssetFiles(asset.files);
|
||||
if (!previewFile) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
const ocrResults = await this.machineLearningRepository.ocr(
|
||||
machineLearning.urls,
|
||||
previewFile.path,
|
||||
machineLearning.ocr
|
||||
);
|
||||
|
||||
if (!ocrResults || ocrResults.text.length === 0) {
|
||||
this.logger.warn(`No OCR results for document ${id}`);
|
||||
await this.assetRepository.upsertJobStatus({
|
||||
assetId: asset.id,
|
||||
ocrAt: new Date(),
|
||||
});
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
this.logger.debug(`OCR ${id} has OCR results`);
|
||||
|
||||
const ocr = await this.ocrRepository.getOcrById(id);
|
||||
if (ocr) {
|
||||
this.logger.debug(`Updating OCR for ${id}`);
|
||||
await this.ocrRepository.updateOcrData(id, ocrResults.text);
|
||||
} else {
|
||||
this.logger.debug(`Inserting OCR for ${id}`);
|
||||
await this.ocrRepository.insertOcrData(id, ocrResults.text);
|
||||
}
|
||||
|
||||
await this.assetRepository.upsertJobStatus({
|
||||
assetId: asset.id,
|
||||
ocrAt: new Date(),
|
||||
});
|
||||
this.logger.debug(`Processed OCR for ${id}`);
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
LargeAssetSearchDto,
|
||||
mapPlaces,
|
||||
MetadataSearchDto,
|
||||
OcrSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
SearchPeopleDto,
|
||||
|
|
@ -22,7 +23,7 @@ import { AssetOrder, AssetVisibility, Permission } from 'src/enum';
|
|||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireElevatedPermission } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||
import { isSmartSearchEnabled, isOcrEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class SearchService extends BaseService {
|
||||
|
|
@ -145,6 +146,23 @@ export class SearchService extends BaseService {
|
|||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||
}
|
||||
|
||||
async searchOcr(auth: AuthDto, dto: OcrSearchDto): Promise<SearchResponseDto> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||
if (!isOcrEnabled(machineLearning)) {
|
||||
throw new BadRequestException('OCR is not enabled');
|
||||
}
|
||||
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
const page = dto.page ?? 1;
|
||||
const size = dto.size || 250;
|
||||
const { items, hasNextPage } = await this.searchRepository.searchOcr(
|
||||
{ page, size },
|
||||
{ ...dto, userIds },
|
||||
);
|
||||
|
||||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||
}
|
||||
|
||||
async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
const assets = await this.searchRepository.getAssetsByCity(userIds);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { UserStatsQueryResponse } from 'src/repositories/user.repository';
|
|||
import { BaseService } from 'src/services/base.service';
|
||||
import { asHumanReadable } from 'src/utils/bytes';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isOcrEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class ServerService extends BaseService {
|
||||
|
|
@ -97,6 +97,7 @@ export class ServerService extends BaseService {
|
|||
trash: trash.enabled,
|
||||
oauth: oauth.enabled,
|
||||
oauthAutoLaunch: oauth.autoLaunch,
|
||||
ocr: isOcrEnabled(machineLearning),
|
||||
passwordLogin: passwordLogin.enabled,
|
||||
configFile: !!configFile,
|
||||
email: notifications.smtp.enabled,
|
||||
|
|
|
|||
|
|
@ -367,7 +367,12 @@ export type JobItem =
|
|||
| { name: JobName.NotifyUserSignup; data: INotifySignupJob }
|
||||
|
||||
// Version check
|
||||
| { name: JobName.VersionCheck; data: IBaseJob };
|
||||
| { name: JobName.VersionCheck; data: IBaseJob }
|
||||
|
||||
// OCR
|
||||
| { name: JobName.QUEUE_OCR; data: INightlyJob }
|
||||
| { name: JobName.OCR; data: IEntityJob }
|
||||
| { name: JobName.OCR_CLEANUP; data?: IBaseJob };
|
||||
|
||||
export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number];
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ export const unsetDeep = (object: unknown, key: string) => {
|
|||
const isMachineLearningEnabled = (machineLearning: SystemConfig['machineLearning']) => machineLearning.enabled;
|
||||
export const isSmartSearchEnabled = (machineLearning: SystemConfig['machineLearning']) =>
|
||||
isMachineLearningEnabled(machineLearning) && machineLearning.clip.enabled;
|
||||
export const isOcrEnabled = (machineLearning: SystemConfig['machineLearning']) =>
|
||||
isMachineLearningEnabled(machineLearning) && machineLearning.ocr.enabled;
|
||||
export const isFacialRecognitionEnabled = (machineLearning: SystemConfig['machineLearning']) =>
|
||||
isMachineLearningEnabled(machineLearning) && machineLearning.facialRecognition.enabled;
|
||||
export const isDuplicateDetectionEnabled = (machineLearning: SystemConfig['machineLearning']) =>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
mdiTable,
|
||||
mdiTagFaces,
|
||||
mdiVideo,
|
||||
mdiOcr,
|
||||
} from '@mdi/js';
|
||||
import type { Component } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
|
@ -124,6 +125,14 @@
|
|||
handleCommand: handleConfirmCommand,
|
||||
disabled: !$featureFlags.facialRecognition,
|
||||
},
|
||||
[JobName.Ocr]: {
|
||||
icon: mdiOcr,
|
||||
title: $getJobName(JobName.Ocr),
|
||||
subtitle: $t('admin.ocr_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
disabled: !$featureFlags.ocr,
|
||||
},
|
||||
[JobName.VideoConversion]: {
|
||||
icon: mdiVideo,
|
||||
title: $getJobName(JobName.VideoConversion),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
JobName.VideoConversion,
|
||||
JobName.StorageTemplateMigration,
|
||||
JobName.Migration,
|
||||
JobName.Ocr,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
|||
|
|
@ -218,6 +218,42 @@
|
|||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="ocr"
|
||||
title={$t('admin.machine_learning_ocr')}
|
||||
subtitle={$t('admin.machine_learning_ocr_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_ocr_enabled')}
|
||||
subtitle={$t('admin.machine_learning_ocr_enabled_description')}
|
||||
bind:checked={config.machineLearning.ocr.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_ocr_model')}
|
||||
desc={$t('admin.machine_learning_ocr_model_description')}
|
||||
name="ocr-model"
|
||||
bind:value={config.machineLearning.ocr.modelName}
|
||||
options={[
|
||||
{ value: 'paddle', text: 'paddle' },
|
||||
]}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled}
|
||||
isEdited={config.machineLearning.ocr.modelName !== savedConfig.machineLearning.ocr.modelName}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_min_score')}
|
||||
bind:value={config.machineLearning.ocr.minScore}
|
||||
step="0.1"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['machineLearning'] })}
|
||||
onSave={() => onSave({ machineLearning: config.machineLearning })}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import { handlePromiseError } from '$lib/utils';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
import type { MetadataSearchDto, OcrSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
import { IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
interface Props {
|
||||
value?: string;
|
||||
grayTheme: boolean;
|
||||
searchQuery?: MetadataSearchDto | SmartSearchDto;
|
||||
searchQuery?: MetadataSearchDto | SmartSearchDto | OcrSearchDto;
|
||||
}
|
||||
|
||||
let { value = $bindable(''), grayTheme, searchQuery = {} }: Props = $props();
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
searchStore.isSearchEnabled = false;
|
||||
});
|
||||
|
||||
const handleSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
|
||||
const handleSearch = async (payload: SmartSearchDto | MetadataSearchDto | OcrSearchDto) => {
|
||||
const params = getMetadataSearchQuery(payload);
|
||||
|
||||
closeDropdown();
|
||||
|
|
@ -107,7 +107,10 @@
|
|||
|
||||
const onSubmit = () => {
|
||||
const searchType = getSearchType();
|
||||
let payload: SmartSearchDto | MetadataSearchDto = {} as SmartSearchDto | MetadataSearchDto;
|
||||
let payload: SmartSearchDto | MetadataSearchDto | OcrSearchDto = {} as
|
||||
| SmartSearchDto
|
||||
| MetadataSearchDto
|
||||
| OcrSearchDto;
|
||||
|
||||
switch (searchType) {
|
||||
case 'smart': {
|
||||
|
|
@ -122,6 +125,10 @@
|
|||
payload = { description: value } as MetadataSearchDto;
|
||||
break;
|
||||
}
|
||||
case 'ocr': {
|
||||
payload = { ocr: value } as OcrSearchDto;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handlePromiseError(handleSearch(payload));
|
||||
|
|
@ -171,7 +178,7 @@
|
|||
onSubmit();
|
||||
};
|
||||
|
||||
function getSearchType(): 'smart' | 'metadata' | 'description' {
|
||||
function getSearchType(): 'smart' | 'metadata' | 'description' | 'ocr' {
|
||||
const searchType = localStorage.getItem('searchQueryType');
|
||||
switch (searchType) {
|
||||
case 'smart': {
|
||||
|
|
@ -183,6 +190,9 @@
|
|||
case 'description': {
|
||||
return 'description';
|
||||
}
|
||||
case 'ocr': {
|
||||
return 'ocr';
|
||||
}
|
||||
default: {
|
||||
return 'smart';
|
||||
}
|
||||
|
|
@ -201,6 +211,9 @@
|
|||
case 'description': {
|
||||
return $t('description');
|
||||
}
|
||||
case 'ocr': {
|
||||
return $t('ocr');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
interface Props {
|
||||
query: string | undefined;
|
||||
queryType?: 'smart' | 'metadata' | 'description';
|
||||
queryType?: 'smart' | 'metadata' | 'description' | 'ocr';
|
||||
}
|
||||
|
||||
let { query = $bindable(), queryType = $bindable('smart') }: Props = $props();
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
bind:group={queryType}
|
||||
value="description"
|
||||
/>
|
||||
<RadioButton name="query-type" id="ocr-radio" label={$t('ocr')} bind:group={queryType} value="ocr" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
|
@ -63,4 +64,15 @@
|
|||
bind:value={query}
|
||||
aria-labelledby="description-label"
|
||||
/>
|
||||
{:else if queryType === 'ocr'}
|
||||
<label for="ocr-input" class="immich-form-label">{$t('search_by_ocr')}</label>
|
||||
<input
|
||||
class="immich-form-input hover:cursor-text w-full !mt-1"
|
||||
type="text"
|
||||
id="ocr-input"
|
||||
name="ocr"
|
||||
placeholder={$t('search_by_ocr_example')}
|
||||
bind:value={query}
|
||||
aria-labelledby="ocr-label"
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -136,9 +136,15 @@ export enum QueryType {
|
|||
SMART = 'smart',
|
||||
METADATA = 'metadata',
|
||||
DESCRIPTION = 'description',
|
||||
OCR = 'ocr',
|
||||
}
|
||||
|
||||
export const validQueryTypes = new Set([QueryType.SMART, QueryType.METADATA, QueryType.DESCRIPTION]);
|
||||
export const validQueryTypes = new Set([
|
||||
QueryType.SMART,
|
||||
QueryType.METADATA,
|
||||
QueryType.DESCRIPTION,
|
||||
QueryType.OCR,
|
||||
]);
|
||||
|
||||
export const locales = [
|
||||
{ code: 'af-ZA', name: 'Afrikaans (South Africa)' },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
export type SearchFilter = {
|
||||
query: string;
|
||||
queryType: 'smart' | 'metadata' | 'description';
|
||||
ocr: string;
|
||||
queryType: 'smart' | 'metadata' | 'description' | 'ocr';
|
||||
personIds: SvelteSet<string>;
|
||||
tagIds: SvelteSet<string> | null;
|
||||
location: SearchLocationFilter;
|
||||
|
|
@ -33,15 +34,15 @@
|
|||
import { preferences } from '$lib/stores/user.store';
|
||||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
|
||||
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type OcrSearchDto, type SmartSearchDto } from '@immich/sdk';
|
||||
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import { mdiTune } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
searchQuery: MetadataSearchDto | SmartSearchDto;
|
||||
onClose: (search?: SmartSearchDto | MetadataSearchDto) => void;
|
||||
searchQuery: MetadataSearchDto | SmartSearchDto | OcrSearchDto;
|
||||
onClose: (search?: SmartSearchDto | MetadataSearchDto | OcrSearchDto) => void;
|
||||
}
|
||||
|
||||
let { searchQuery, onClose }: Props = $props();
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
|
||||
let filter: SearchFilter = $state({
|
||||
query,
|
||||
ocr: 'ocr' in searchQuery ? searchQuery.ocr : '',
|
||||
queryType: defaultQueryType(),
|
||||
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
|
||||
tagIds:
|
||||
|
|
@ -112,6 +114,7 @@
|
|||
const resetForm = () => {
|
||||
filter = {
|
||||
query: '',
|
||||
ocr: '',
|
||||
queryType: defaultQueryType(), // retain from localStorage or default
|
||||
personIds: new SvelteSet(),
|
||||
tagIds: new SvelteSet(),
|
||||
|
|
@ -138,8 +141,9 @@
|
|||
|
||||
const query = filter.query || undefined;
|
||||
|
||||
let payload: SmartSearchDto | MetadataSearchDto = {
|
||||
let payload: SmartSearchDto | MetadataSearchDto | OcrSearchDto = {
|
||||
query: filter.queryType === 'smart' ? query : undefined,
|
||||
ocr: filter.queryType === 'ocr' ? query : undefined,
|
||||
originalFileName: filter.queryType === 'metadata' ? query : undefined,
|
||||
description: filter.queryType === 'description' ? query : undefined,
|
||||
country: filter.location.country,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const featureFlags = writable<FeatureFlags>({
|
|||
configFile: false,
|
||||
trash: true,
|
||||
email: false,
|
||||
ocr: true,
|
||||
});
|
||||
|
||||
export type ServerConfig = ServerConfigDto & { loaded: boolean };
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ export const getJobName = derived(t, ($t) => {
|
|||
[JobName.Library]: $t('external_libraries'),
|
||||
[JobName.Notifications]: $t('notifications'),
|
||||
[JobName.BackupDatabase]: $t('admin.backup_database'),
|
||||
[JobName.Ocr]: $t('admin.machine_learning_ocr'),
|
||||
};
|
||||
|
||||
return names[jobName];
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@
|
|||
getPerson,
|
||||
getTagById,
|
||||
type MetadataSearchDto,
|
||||
type OcrSearchDto,
|
||||
searchAssets,
|
||||
searchOcr,
|
||||
searchSmart,
|
||||
type SmartSearchDto,
|
||||
} from '@immich/sdk';
|
||||
|
|
@ -68,7 +70,7 @@
|
|||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query' | 'queryAssetId'>;
|
||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query' | 'queryAssetId'> & Pick<OcrSearchDto, 'ocr'>;
|
||||
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
|
||||
let smartSearchEnabled = $derived($featureFlags.loaded && $featureFlags.smartSearch);
|
||||
let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {});
|
||||
|
|
@ -164,7 +166,9 @@
|
|||
|
||||
try {
|
||||
const { albums, assets } =
|
||||
('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
|
||||
('ocr' in searchDto
|
||||
? await searchOcr({ ocrSearchDto: searchDto })
|
||||
: 'query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
|
||||
? await searchSmart({ smartSearchDto: searchDto })
|
||||
: await searchAssets({ metadataSearchDto: searchDto });
|
||||
|
||||
|
|
@ -211,6 +215,7 @@
|
|||
originalFileName: $t('file_name'),
|
||||
description: $t('description'),
|
||||
queryAssetId: $t('query_asset_id'),
|
||||
ocr: $t('ocr'),
|
||||
};
|
||||
return keyMap[key] || key;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue