mirror of
https://github.com/immich-app/immich.git
synced 2025-11-02 09:07:42 +09:00
feat: availability checks (#22185)
This commit is contained in:
@@ -169,8 +169,6 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||||
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
|
||||||
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
|
||||||
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
|
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
|
||||||
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning |
|
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning |
|
||||||
|
|
||||||
|
@@ -123,6 +123,13 @@
|
|||||||
"logging_enable_description": "Enable logging",
|
"logging_enable_description": "Enable logging",
|
||||||
"logging_level_description": "When enabled, what log level to use.",
|
"logging_level_description": "When enabled, what log level to use.",
|
||||||
"logging_settings": "Logging",
|
"logging_settings": "Logging",
|
||||||
|
"machine_learning_availability_checks": "Availability checks",
|
||||||
|
"machine_learning_availability_checks_description": "Automatically detect and prefer available machine learning servers",
|
||||||
|
"machine_learning_availability_checks_enabled": "Enable availability checks",
|
||||||
|
"machine_learning_availability_checks_interval": "Check interval",
|
||||||
|
"machine_learning_availability_checks_interval_description": "Interval in milliseconds between availability checks",
|
||||||
|
"machine_learning_availability_checks_timeout": "Request timeout",
|
||||||
|
"machine_learning_availability_checks_timeout_description": "Timeout in milliseconds for availability checks",
|
||||||
"machine_learning_clip_model": "CLIP model",
|
"machine_learning_clip_model": "CLIP model",
|
||||||
"machine_learning_clip_model_description": "The name of a CLIP model listed <link>here</link>. Note that you must re-run the 'Smart Search' job for all images upon changing a model.",
|
"machine_learning_clip_model_description": "The name of a CLIP model listed <link>here</link>. Note that you must re-run the 'Smart Search' job for all images upon changing a model.",
|
||||||
"machine_learning_duplicate_detection": "Duplicate Detection",
|
"machine_learning_duplicate_detection": "Duplicate Detection",
|
||||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -393,6 +393,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
||||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||||
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
||||||
|
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
|
||||||
- [ManualJobName](doc//ManualJobName.md)
|
- [ManualJobName](doc//ManualJobName.md)
|
||||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||||
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -164,6 +164,7 @@ part 'model/log_level.dart';
|
|||||||
part 'model/login_credential_dto.dart';
|
part 'model/login_credential_dto.dart';
|
||||||
part 'model/login_response_dto.dart';
|
part 'model/login_response_dto.dart';
|
||||||
part 'model/logout_response_dto.dart';
|
part 'model/logout_response_dto.dart';
|
||||||
|
part 'model/machine_learning_availability_checks_dto.dart';
|
||||||
part 'model/manual_job_name.dart';
|
part 'model/manual_job_name.dart';
|
||||||
part 'model/map_marker_response_dto.dart';
|
part 'model/map_marker_response_dto.dart';
|
||||||
part 'model/map_reverse_geocode_response_dto.dart';
|
part 'model/map_reverse_geocode_response_dto.dart';
|
||||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -382,6 +382,8 @@ class ApiClient {
|
|||||||
return LoginResponseDto.fromJson(value);
|
return LoginResponseDto.fromJson(value);
|
||||||
case 'LogoutResponseDto':
|
case 'LogoutResponseDto':
|
||||||
return LogoutResponseDto.fromJson(value);
|
return LogoutResponseDto.fromJson(value);
|
||||||
|
case 'MachineLearningAvailabilityChecksDto':
|
||||||
|
return MachineLearningAvailabilityChecksDto.fromJson(value);
|
||||||
case 'ManualJobName':
|
case 'ManualJobName':
|
||||||
return ManualJobNameTypeTransformer().decode(value);
|
return ManualJobNameTypeTransformer().decode(value);
|
||||||
case 'MapMarkerResponseDto':
|
case 'MapMarkerResponseDto':
|
||||||
|
115
mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// 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 MachineLearningAvailabilityChecksDto {
|
||||||
|
/// Returns a new [MachineLearningAvailabilityChecksDto] instance.
|
||||||
|
MachineLearningAvailabilityChecksDto({
|
||||||
|
required this.enabled,
|
||||||
|
required this.interval,
|
||||||
|
required this.timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool enabled;
|
||||||
|
|
||||||
|
num interval;
|
||||||
|
|
||||||
|
num timeout;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
|
||||||
|
other.enabled == enabled &&
|
||||||
|
other.interval == interval &&
|
||||||
|
other.timeout == timeout;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(enabled.hashCode) +
|
||||||
|
(interval.hashCode) +
|
||||||
|
(timeout.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MachineLearningAvailabilityChecksDto[enabled=$enabled, interval=$interval, timeout=$timeout]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'enabled'] = this.enabled;
|
||||||
|
json[r'interval'] = this.interval;
|
||||||
|
json[r'timeout'] = this.timeout;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MachineLearningAvailabilityChecksDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MachineLearningAvailabilityChecksDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MachineLearningAvailabilityChecksDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MachineLearningAvailabilityChecksDto(
|
||||||
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
|
interval: num.parse('${json[r'interval']}'),
|
||||||
|
timeout: num.parse('${json[r'timeout']}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MachineLearningAvailabilityChecksDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MachineLearningAvailabilityChecksDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MachineLearningAvailabilityChecksDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MachineLearningAvailabilityChecksDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MachineLearningAvailabilityChecksDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MachineLearningAvailabilityChecksDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MachineLearningAvailabilityChecksDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MachineLearningAvailabilityChecksDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MachineLearningAvailabilityChecksDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MachineLearningAvailabilityChecksDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'enabled',
|
||||||
|
'interval',
|
||||||
|
'timeout',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@@ -13,14 +13,16 @@ part of openapi.api;
|
|||||||
class SystemConfigMachineLearningDto {
|
class SystemConfigMachineLearningDto {
|
||||||
/// Returns a new [SystemConfigMachineLearningDto] instance.
|
/// Returns a new [SystemConfigMachineLearningDto] instance.
|
||||||
SystemConfigMachineLearningDto({
|
SystemConfigMachineLearningDto({
|
||||||
|
required this.availabilityChecks,
|
||||||
required this.clip,
|
required this.clip,
|
||||||
required this.duplicateDetection,
|
required this.duplicateDetection,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.facialRecognition,
|
required this.facialRecognition,
|
||||||
this.url,
|
|
||||||
this.urls = const [],
|
this.urls = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MachineLearningAvailabilityChecksDto availabilityChecks;
|
||||||
|
|
||||||
CLIPConfig clip;
|
CLIPConfig clip;
|
||||||
|
|
||||||
DuplicateDetectionConfig duplicateDetection;
|
DuplicateDetectionConfig duplicateDetection;
|
||||||
@@ -29,50 +31,37 @@ class SystemConfigMachineLearningDto {
|
|||||||
|
|
||||||
FacialRecognitionConfig facialRecognition;
|
FacialRecognitionConfig facialRecognition;
|
||||||
|
|
||||||
/// This property was deprecated in v1.122.0
|
|
||||||
///
|
|
||||||
/// 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? url;
|
|
||||||
|
|
||||||
List<String> urls;
|
List<String> urls;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto &&
|
||||||
|
other.availabilityChecks == availabilityChecks &&
|
||||||
other.clip == clip &&
|
other.clip == clip &&
|
||||||
other.duplicateDetection == duplicateDetection &&
|
other.duplicateDetection == duplicateDetection &&
|
||||||
other.enabled == enabled &&
|
other.enabled == enabled &&
|
||||||
other.facialRecognition == facialRecognition &&
|
other.facialRecognition == facialRecognition &&
|
||||||
other.url == url &&
|
|
||||||
_deepEquality.equals(other.urls, urls);
|
_deepEquality.equals(other.urls, urls);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(availabilityChecks.hashCode) +
|
||||||
(clip.hashCode) +
|
(clip.hashCode) +
|
||||||
(duplicateDetection.hashCode) +
|
(duplicateDetection.hashCode) +
|
||||||
(enabled.hashCode) +
|
(enabled.hashCode) +
|
||||||
(facialRecognition.hashCode) +
|
(facialRecognition.hashCode) +
|
||||||
(url == null ? 0 : url!.hashCode) +
|
|
||||||
(urls.hashCode);
|
(urls.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigMachineLearningDto[clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, url=$url, urls=$urls]';
|
String toString() => 'SystemConfigMachineLearningDto[availabilityChecks=$availabilityChecks, clip=$clip, duplicateDetection=$duplicateDetection, enabled=$enabled, facialRecognition=$facialRecognition, urls=$urls]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
json[r'availabilityChecks'] = this.availabilityChecks;
|
||||||
json[r'clip'] = this.clip;
|
json[r'clip'] = this.clip;
|
||||||
json[r'duplicateDetection'] = this.duplicateDetection;
|
json[r'duplicateDetection'] = this.duplicateDetection;
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
json[r'facialRecognition'] = this.facialRecognition;
|
json[r'facialRecognition'] = this.facialRecognition;
|
||||||
if (this.url != null) {
|
|
||||||
json[r'url'] = this.url;
|
|
||||||
} else {
|
|
||||||
// json[r'url'] = null;
|
|
||||||
}
|
|
||||||
json[r'urls'] = this.urls;
|
json[r'urls'] = this.urls;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -86,11 +75,11 @@ class SystemConfigMachineLearningDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return SystemConfigMachineLearningDto(
|
return SystemConfigMachineLearningDto(
|
||||||
|
availabilityChecks: MachineLearningAvailabilityChecksDto.fromJson(json[r'availabilityChecks'])!,
|
||||||
clip: CLIPConfig.fromJson(json[r'clip'])!,
|
clip: CLIPConfig.fromJson(json[r'clip'])!,
|
||||||
duplicateDetection: DuplicateDetectionConfig.fromJson(json[r'duplicateDetection'])!,
|
duplicateDetection: DuplicateDetectionConfig.fromJson(json[r'duplicateDetection'])!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
facialRecognition: FacialRecognitionConfig.fromJson(json[r'facialRecognition'])!,
|
facialRecognition: FacialRecognitionConfig.fromJson(json[r'facialRecognition'])!,
|
||||||
url: mapValueOfType<String>(json, r'url'),
|
|
||||||
urls: json[r'urls'] is Iterable
|
urls: json[r'urls'] is Iterable
|
||||||
? (json[r'urls'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'urls'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
@@ -141,6 +130,7 @@ class SystemConfigMachineLearningDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
|
'availabilityChecks',
|
||||||
'clip',
|
'clip',
|
||||||
'duplicateDetection',
|
'duplicateDetection',
|
||||||
'enabled',
|
'enabled',
|
||||||
|
@@ -12259,6 +12259,25 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MachineLearningAvailabilityChecksDto": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"interval",
|
||||||
|
"timeout"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ManualJobName": {
|
"ManualJobName": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"person-cleanup",
|
"person-cleanup",
|
||||||
@@ -16395,6 +16414,9 @@
|
|||||||
},
|
},
|
||||||
"SystemConfigMachineLearningDto": {
|
"SystemConfigMachineLearningDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"availabilityChecks": {
|
||||||
|
"$ref": "#/components/schemas/MachineLearningAvailabilityChecksDto"
|
||||||
|
},
|
||||||
"clip": {
|
"clip": {
|
||||||
"$ref": "#/components/schemas/CLIPConfig"
|
"$ref": "#/components/schemas/CLIPConfig"
|
||||||
},
|
},
|
||||||
@@ -16407,11 +16429,6 @@
|
|||||||
"facialRecognition": {
|
"facialRecognition": {
|
||||||
"$ref": "#/components/schemas/FacialRecognitionConfig"
|
"$ref": "#/components/schemas/FacialRecognitionConfig"
|
||||||
},
|
},
|
||||||
"url": {
|
|
||||||
"deprecated": true,
|
|
||||||
"description": "This property was deprecated in v1.122.0",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"urls": {
|
"urls": {
|
||||||
"format": "uri",
|
"format": "uri",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -16423,6 +16440,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"availabilityChecks",
|
||||||
"clip",
|
"clip",
|
||||||
"duplicateDetection",
|
"duplicateDetection",
|
||||||
"enabled",
|
"enabled",
|
||||||
|
@@ -1383,6 +1383,11 @@ export type SystemConfigLoggingDto = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
level: LogLevel;
|
level: LogLevel;
|
||||||
};
|
};
|
||||||
|
export type MachineLearningAvailabilityChecksDto = {
|
||||||
|
enabled: boolean;
|
||||||
|
interval: number;
|
||||||
|
timeout: number;
|
||||||
|
};
|
||||||
export type ClipConfig = {
|
export type ClipConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
@@ -1399,12 +1404,11 @@ export type FacialRecognitionConfig = {
|
|||||||
modelName: string;
|
modelName: string;
|
||||||
};
|
};
|
||||||
export type SystemConfigMachineLearningDto = {
|
export type SystemConfigMachineLearningDto = {
|
||||||
|
availabilityChecks: MachineLearningAvailabilityChecksDto;
|
||||||
clip: ClipConfig;
|
clip: ClipConfig;
|
||||||
duplicateDetection: DuplicateDetectionConfig;
|
duplicateDetection: DuplicateDetectionConfig;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
facialRecognition: FacialRecognitionConfig;
|
facialRecognition: FacialRecognitionConfig;
|
||||||
/** This property was deprecated in v1.122.0 */
|
|
||||||
url?: string;
|
|
||||||
urls: string[];
|
urls: string[];
|
||||||
};
|
};
|
||||||
export type SystemConfigMapDto = {
|
export type SystemConfigMapDto = {
|
||||||
|
@@ -15,6 +15,7 @@ import { repositories } from 'src/repositories';
|
|||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { getKyselyConfig } from 'src/utils/database';
|
import { getKyselyConfig } from 'src/utils/database';
|
||||||
@@ -57,7 +58,7 @@ class SqlGenerator {
|
|||||||
try {
|
try {
|
||||||
await this.setup();
|
await this.setup();
|
||||||
for (const Repository of repositories) {
|
for (const Repository of repositories) {
|
||||||
if (Repository === LoggingRepository) {
|
if (Repository === LoggingRepository || Repository === MachineLearningRepository) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await this.process(Repository);
|
await this.process(Repository);
|
||||||
|
@@ -54,6 +54,11 @@ export interface SystemConfig {
|
|||||||
machineLearning: {
|
machineLearning: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
urls: string[];
|
urls: string[];
|
||||||
|
availabilityChecks: {
|
||||||
|
enabled: boolean;
|
||||||
|
timeout: number;
|
||||||
|
interval: number;
|
||||||
|
};
|
||||||
clip: {
|
clip: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
@@ -176,6 +181,8 @@ export interface SystemConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MachineLearningConfig = SystemConfig['machineLearning'];
|
||||||
|
|
||||||
export const defaults = Object.freeze<SystemConfig>({
|
export const defaults = Object.freeze<SystemConfig>({
|
||||||
backup: {
|
backup: {
|
||||||
database: {
|
database: {
|
||||||
@@ -227,6 +234,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
machineLearning: {
|
machineLearning: {
|
||||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||||
urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'],
|
urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'],
|
||||||
|
availabilityChecks: {
|
||||||
|
enabled: true,
|
||||||
|
timeout: Number(process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT) || 2000,
|
||||||
|
interval: 30_000,
|
||||||
|
},
|
||||||
clip: {
|
clip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
modelName: 'ViT-B-32__openai',
|
modelName: 'ViT-B-32__openai',
|
||||||
|
@@ -51,11 +51,6 @@ export const serverVersion = new SemVer(version);
|
|||||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||||
|
|
||||||
export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000);
|
|
||||||
export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number(
|
|
||||||
process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const citiesFile = 'cities500.txt';
|
export const citiesFile = 'cities500.txt';
|
||||||
export const reverseGeocodeMaxDistance = 25_000;
|
export const reverseGeocodeMaxDistance = 25_000;
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Exclude, Transform, Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
ArrayMinSize,
|
ArrayMinSize,
|
||||||
IsInt,
|
IsInt,
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { PropertyLifecycle } from 'src/decorators';
|
|
||||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
@@ -257,21 +256,32 @@ class SystemConfigLoggingDto {
|
|||||||
level!: LogLevel;
|
level!: LogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MachineLearningAvailabilityChecksDto {
|
||||||
|
@ValidateBoolean()
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
timeout!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
interval!: number;
|
||||||
|
}
|
||||||
|
|
||||||
class SystemConfigMachineLearningDto {
|
class SystemConfigMachineLearningDto {
|
||||||
@ValidateBoolean()
|
@ValidateBoolean()
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
|
|
||||||
@PropertyLifecycle({ deprecatedAt: 'v1.122.0' })
|
|
||||||
@Exclude()
|
|
||||||
url?: string;
|
|
||||||
|
|
||||||
@IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
|
@IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
|
||||||
@ArrayMinSize(1)
|
@ArrayMinSize(1)
|
||||||
@Transform(({ obj, value }) => (obj.url ? [obj.url] : value))
|
|
||||||
@ValidateIf((dto) => dto.enabled)
|
@ValidateIf((dto) => dto.enabled)
|
||||||
@ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 })
|
@ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 })
|
||||||
urls!: string[];
|
urls!: string[];
|
||||||
|
|
||||||
|
@Type(() => MachineLearningAvailabilityChecksDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
availabilityChecks!: MachineLearningAvailabilityChecksDto;
|
||||||
|
|
||||||
@Type(() => CLIPConfig)
|
@Type(() => CLIPConfig)
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
|
@@ -142,6 +142,10 @@ export class LoggingRepository {
|
|||||||
this.handleMessage(LogLevel.Fatal, message, details);
|
this.handleMessage(LogLevel.Fatal, message, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deprecate(message: string) {
|
||||||
|
this.warn(`[Deprecated] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) {
|
private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) {
|
||||||
if (this.logger.isLevelEnabled(level)) {
|
if (this.logger.isLevelEnabled(level)) {
|
||||||
this.handleMessage(level, message(), details);
|
this.handleMessage(level, message(), details);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME, MACHINE_LEARNING_PING_TIMEOUT } from 'src/constants';
|
import { MachineLearningConfig } from 'src/config';
|
||||||
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
|
||||||
@@ -57,82 +58,100 @@ export type TextEncodingOptions = ModelOptions & { language?: string };
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MachineLearningRepository {
|
export class MachineLearningRepository {
|
||||||
// Note that deleted URL's are not removed from this map (ie: they're leaked)
|
private healthyMap: Record<string, boolean> = {};
|
||||||
// Cleaning them up is low priority since there should be very few over a
|
private interval?: ReturnType<typeof setInterval>;
|
||||||
// typical server uptime cycle
|
private _config?: MachineLearningConfig;
|
||||||
private urlAvailability: {
|
|
||||||
[url: string]:
|
private get config(): MachineLearningConfig {
|
||||||
| {
|
if (!this._config) {
|
||||||
active: boolean;
|
throw new Error('Machine learning repository not been setup');
|
||||||
lastChecked: number;
|
}
|
||||||
}
|
|
||||||
| undefined;
|
return this._config;
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(private logger: LoggingRepository) {
|
||||||
this.logger.setContext(MachineLearningRepository.name);
|
this.logger.setContext(MachineLearningRepository.name);
|
||||||
this.urlAvailability = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUrlAvailability(url: string, active: boolean) {
|
setup(config: MachineLearningConfig) {
|
||||||
const current = this.urlAvailability[url];
|
this._config = config;
|
||||||
if (current?.active !== active) {
|
this.teardown();
|
||||||
this.logger.verbose(`Setting ${url} ML server to ${active ? 'active' : 'inactive'}.`);
|
|
||||||
|
// delete old servers
|
||||||
|
for (const url of Object.keys(this.healthyMap)) {
|
||||||
|
if (!config.urls.includes(url)) {
|
||||||
|
delete this.healthyMap[url];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.urlAvailability[url] = {
|
|
||||||
active,
|
if (!config.availabilityChecks.enabled) {
|
||||||
lastChecked: Date.now(),
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
this.tick();
|
||||||
|
this.interval = setInterval(
|
||||||
|
() => this.tick(),
|
||||||
|
Duration.fromObject({ milliseconds: config.availabilityChecks.interval }).as('milliseconds'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkAvailability(url: string) {
|
teardown() {
|
||||||
let active = false;
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick() {
|
||||||
|
for (const url of this.config.urls) {
|
||||||
|
void this.check(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async check(url: string) {
|
||||||
|
let healthy = false;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(new URL('/ping', url), {
|
const response = await fetch(new URL('/ping', url), {
|
||||||
signal: AbortSignal.timeout(MACHINE_LEARNING_PING_TIMEOUT),
|
signal: AbortSignal.timeout(this.config.availabilityChecks.timeout),
|
||||||
});
|
});
|
||||||
active = response.ok;
|
if (response.ok) {
|
||||||
|
healthy = true;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// nothing to do here
|
// nothing to do here
|
||||||
}
|
}
|
||||||
this.setUrlAvailability(url, active);
|
|
||||||
return active;
|
this.setHealthy(url, healthy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async shouldSkipUrl(url: string) {
|
private setHealthy(url: string, healthy: boolean) {
|
||||||
const availability = this.urlAvailability[url];
|
if (this.healthyMap[url] !== healthy) {
|
||||||
if (availability === undefined) {
|
this.logger.log(`Machine learning server became ${healthy ? 'healthy' : 'unhealthy'} (${url}).`);
|
||||||
// If this is a new endpoint, then check inline and skip if it fails
|
|
||||||
if (!(await this.checkAvailability(url))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (!availability.active && Date.now() - availability.lastChecked < MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME) {
|
|
||||||
// If this is an old inactive endpoint that hasn't been checked in a
|
this.healthyMap[url] = healthy;
|
||||||
// while then check but don't wait for the result, just skip it
|
}
|
||||||
// This avoids delays on every search whilst allowing higher priority
|
|
||||||
// ML servers to recover over time.
|
private isHealthy(url: string) {
|
||||||
void this.checkAvailability(url);
|
if (!this.config.availabilityChecks.enabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return this.healthyMap[url];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async predict<T>(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
private async predict<T>(payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
||||||
const formData = await this.getFormData(payload, config);
|
const formData = await this.getFormData(payload, config);
|
||||||
let urlCounter = 0;
|
|
||||||
for (const url of urls) {
|
|
||||||
urlCounter++;
|
|
||||||
const isLast = urlCounter >= urls.length;
|
|
||||||
if (!isLast && (await this.shouldSkipUrl(url))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (const url of [
|
||||||
|
// try healthy servers first
|
||||||
|
...this.config.urls.filter((url) => this.isHealthy(url)),
|
||||||
|
...this.config.urls.filter((url) => !this.isHealthy(url)),
|
||||||
|
]) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this.setUrlAvailability(url, true);
|
this.setHealthy(url, true);
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,20 +163,21 @@ export class MachineLearningRepository {
|
|||||||
`Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`,
|
`Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setUrlAvailability(url, false);
|
|
||||||
|
this.setHealthy(url, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
|
throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectFaces(urls: string[], imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
|
async detectFaces(imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
|
||||||
const request = {
|
const request = {
|
||||||
[ModelTask.FACIAL_RECOGNITION]: {
|
[ModelTask.FACIAL_RECOGNITION]: {
|
||||||
[ModelType.DETECTION]: { modelName, options: { minScore } },
|
[ModelType.DETECTION]: { modelName, options: { minScore } },
|
||||||
[ModelType.RECOGNITION]: { modelName },
|
[ModelType.RECOGNITION]: { modelName },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const response = await this.predict<FacialRecognitionResponse>(urls, { imagePath }, request);
|
const response = await this.predict<FacialRecognitionResponse>({ imagePath }, request);
|
||||||
return {
|
return {
|
||||||
imageHeight: response.imageHeight,
|
imageHeight: response.imageHeight,
|
||||||
imageWidth: response.imageWidth,
|
imageWidth: response.imageWidth,
|
||||||
@@ -165,15 +185,15 @@ export class MachineLearningRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async encodeImage(urls: string[], imagePath: string, { modelName }: CLIPConfig) {
|
async encodeImage(imagePath: string, { modelName }: CLIPConfig) {
|
||||||
const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } };
|
const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } };
|
||||||
const response = await this.predict<ClipVisualResponse>(urls, { imagePath }, request);
|
const response = await this.predict<ClipVisualResponse>({ imagePath }, request);
|
||||||
return response[ModelTask.SEARCH];
|
return response[ModelTask.SEARCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
async encodeText(urls: string[], text: string, { language, modelName }: TextEncodingOptions) {
|
async encodeText(text: string, { language, modelName }: TextEncodingOptions) {
|
||||||
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } };
|
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } };
|
||||||
const response = await this.predict<ClipTextualResponse>(urls, { text }, request);
|
const response = await this.predict<ClipTextualResponse>({ text }, request);
|
||||||
return response[ModelTask.SEARCH];
|
return response[ModelTask.SEARCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -729,7 +729,6 @@ describe(PersonService.name, () => {
|
|||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] });
|
||||||
await sut.handleDetectFaces({ id: assetStub.image.id });
|
await sut.handleDetectFaces({ id: assetStub.image.id });
|
||||||
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
||||||
['http://immich-machine-learning:3003'],
|
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
||||||
);
|
);
|
||||||
|
@@ -316,7 +316,6 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces(
|
const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces(
|
||||||
machineLearning.urls,
|
|
||||||
previewFile.path,
|
previewFile.path,
|
||||||
machineLearning.facialRecognition,
|
machineLearning.facialRecognition,
|
||||||
);
|
);
|
||||||
|
@@ -211,7 +211,6 @@ describe(SearchService.name, () => {
|
|||||||
await sut.searchSmart(authStub.user1, { query: 'test' });
|
await sut.searchSmart(authStub.user1, { query: 'test' });
|
||||||
|
|
||||||
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
||||||
[expect.any(String)],
|
|
||||||
'test',
|
'test',
|
||||||
expect.objectContaining({ modelName: expect.any(String) }),
|
expect.objectContaining({ modelName: expect.any(String) }),
|
||||||
);
|
);
|
||||||
@@ -225,7 +224,6 @@ describe(SearchService.name, () => {
|
|||||||
await sut.searchSmart(authStub.user1, { query: 'test', page: 2, size: 50 });
|
await sut.searchSmart(authStub.user1, { query: 'test', page: 2, size: 50 });
|
||||||
|
|
||||||
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
||||||
[expect.any(String)],
|
|
||||||
'test',
|
'test',
|
||||||
expect.objectContaining({ modelName: expect.any(String) }),
|
expect.objectContaining({ modelName: expect.any(String) }),
|
||||||
);
|
);
|
||||||
@@ -243,7 +241,6 @@ describe(SearchService.name, () => {
|
|||||||
await sut.searchSmart(authStub.user1, { query: 'test' });
|
await sut.searchSmart(authStub.user1, { query: 'test' });
|
||||||
|
|
||||||
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
||||||
[expect.any(String)],
|
|
||||||
'test',
|
'test',
|
||||||
expect.objectContaining({ modelName: 'ViT-B-16-SigLIP__webli' }),
|
expect.objectContaining({ modelName: 'ViT-B-16-SigLIP__webli' }),
|
||||||
);
|
);
|
||||||
@@ -253,7 +250,6 @@ describe(SearchService.name, () => {
|
|||||||
await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' });
|
await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' });
|
||||||
|
|
||||||
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
|
||||||
[expect.any(String)],
|
|
||||||
'test',
|
'test',
|
||||||
expect.objectContaining({ language: 'de' }),
|
expect.objectContaining({ language: 'de' }),
|
||||||
);
|
);
|
||||||
|
@@ -118,7 +118,7 @@ export class SearchService extends BaseService {
|
|||||||
const key = machineLearning.clip.modelName + dto.query + dto.language;
|
const key = machineLearning.clip.modelName + dto.query + dto.language;
|
||||||
embedding = this.embeddingCache.get(key);
|
embedding = this.embeddingCache.get(key);
|
||||||
if (!embedding) {
|
if (!embedding) {
|
||||||
embedding = await this.machineLearningRepository.encodeText(machineLearning.urls, dto.query, {
|
embedding = await this.machineLearningRepository.encodeText(dto.query, {
|
||||||
modelName: machineLearning.clip.modelName,
|
modelName: machineLearning.clip.modelName,
|
||||||
language: dto.language,
|
language: dto.language,
|
||||||
});
|
});
|
||||||
|
@@ -205,7 +205,6 @@ describe(SmartInfoService.name, () => {
|
|||||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success);
|
||||||
|
|
||||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||||
['http://immich-machine-learning:3003'],
|
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||||
);
|
);
|
||||||
@@ -242,7 +241,6 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.database.wait).toHaveBeenCalledWith(512);
|
expect(mocks.database.wait).toHaveBeenCalledWith(512);
|
||||||
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith(
|
||||||
['http://immich-machine-learning:3003'],
|
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||||
);
|
);
|
||||||
|
@@ -108,11 +108,7 @@ export class SmartInfoService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedding = await this.machineLearningRepository.encodeImage(
|
const embedding = await this.machineLearningRepository.encodeImage(asset.files[0].path, machineLearning.clip);
|
||||||
machineLearning.urls,
|
|
||||||
asset.files[0].path,
|
|
||||||
machineLearning.clip,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.databaseRepository.isBusy(DatabaseLock.CLIPDimSize)) {
|
if (this.databaseRepository.isBusy(DatabaseLock.CLIPDimSize)) {
|
||||||
this.logger.verbose(`Waiting for CLIP dimension size to be updated`);
|
this.logger.verbose(`Waiting for CLIP dimension size to be updated`);
|
||||||
|
@@ -82,6 +82,11 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
machineLearning: {
|
machineLearning: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
urls: ['http://immich-machine-learning:3003'],
|
urls: ['http://immich-machine-learning:3003'],
|
||||||
|
availabilityChecks: {
|
||||||
|
enabled: true,
|
||||||
|
interval: 30_000,
|
||||||
|
timeout: 2000,
|
||||||
|
},
|
||||||
clip: {
|
clip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
modelName: 'ViT-B-32__openai',
|
modelName: 'ViT-B-32__openai',
|
||||||
|
@@ -16,6 +16,20 @@ export class SystemConfigService extends BaseService {
|
|||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.eventRepository.emit('ConfigInit', { newConfig: config });
|
await this.eventRepository.emit('ConfigInit', { newConfig: config });
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.IMMICH_MACHINE_LEARNING_PING_TIMEOUT ||
|
||||||
|
process.env.IMMICH_MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME
|
||||||
|
) {
|
||||||
|
this.logger.deprecate(
|
||||||
|
'IMMICH_MACHINE_LEARNING_PING_TIMEOUT and MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME have been moved to system config(`machineLearning.availabilityChecks`) and will be removed in a future release.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'AppShutdown' })
|
||||||
|
onShutdown() {
|
||||||
|
this.machineLearningRepository.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSystemConfig(): Promise<SystemConfigDto> {
|
async getSystemConfig(): Promise<SystemConfigDto> {
|
||||||
@@ -28,12 +42,14 @@ export class SystemConfigService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'ConfigInit', priority: -100 })
|
@OnEvent({ name: 'ConfigInit', priority: -100 })
|
||||||
onConfigInit({ newConfig: { logging } }: ArgOf<'ConfigInit'>) {
|
onConfigInit({ newConfig: { logging, machineLearning } }: ArgOf<'ConfigInit'>) {
|
||||||
const { logLevel: envLevel } = this.configRepository.getEnv();
|
const { logLevel: envLevel } = this.configRepository.getEnv();
|
||||||
const configLevel = logging.enabled ? logging.level : false;
|
const configLevel = logging.enabled ? logging.level : false;
|
||||||
const level = envLevel ?? configLevel;
|
const level = envLevel ?? configLevel;
|
||||||
this.logger.setLogLevel(level);
|
this.logger.setLogLevel(level);
|
||||||
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
||||||
|
|
||||||
|
this.machineLearningRepository.setup(machineLearning);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import type { SystemConfigDto } from '@immich/sdk';
|
import type { SystemConfigDto } from '@immich/sdk';
|
||||||
import { Button, IconButton } from '@immich/ui';
|
import { Button, IconButton } from '@immich/ui';
|
||||||
import { mdiMinusCircle } from '@mdi/js';
|
import { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -46,19 +46,6 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#each config.machineLearning.urls as _, i (i)}
|
{#each config.machineLearning.urls as _, i (i)}
|
||||||
{#snippet removeButton()}
|
|
||||||
{#if config.machineLearning.urls.length > 1}
|
|
||||||
<IconButton
|
|
||||||
size="large"
|
|
||||||
shape="round"
|
|
||||||
color="danger"
|
|
||||||
aria-label=""
|
|
||||||
onclick={() => config.machineLearning.urls.splice(i, 1)}
|
|
||||||
icon={mdiMinusCircle}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label={i === 0 ? $t('url') : undefined}
|
label={i === 0 ? $t('url') : undefined}
|
||||||
@@ -67,20 +54,69 @@
|
|||||||
required={i === 0}
|
required={i === 0}
|
||||||
disabled={disabled || !config.machineLearning.enabled}
|
disabled={disabled || !config.machineLearning.enabled}
|
||||||
isEdited={i === 0 && !isEqual(config.machineLearning.urls, savedConfig.machineLearning.urls)}
|
isEdited={i === 0 && !isEqual(config.machineLearning.urls, savedConfig.machineLearning.urls)}
|
||||||
trailingSnippet={removeButton}
|
>
|
||||||
/>
|
{#snippet trailingSnippet()}
|
||||||
|
{#if config.machineLearning.urls.length > 1}
|
||||||
|
<IconButton
|
||||||
|
aria-label=""
|
||||||
|
onclick={() => config.machineLearning.urls.splice(i, 1)}
|
||||||
|
icon={mdiTrashCanOutline}
|
||||||
|
color="danger"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</SettingInputField>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div class="flex justify-end">
|
||||||
class="mb-2"
|
<Button
|
||||||
size="small"
|
class="mb-2"
|
||||||
shape="round"
|
size="small"
|
||||||
onclick={() => config.machineLearning.urls.splice(0, 0, '')}
|
shape="round"
|
||||||
disabled={disabled || !config.machineLearning.enabled}>{$t('add_url')}</Button
|
leadingIcon={mdiPlus}
|
||||||
>
|
onclick={() => config.machineLearning.urls.push('')}
|
||||||
|
disabled={disabled || !config.machineLearning.enabled}>{$t('add_url')}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SettingAccordion
|
||||||
|
key="availability-checks"
|
||||||
|
title={$t('admin.machine_learning_availability_checks')}
|
||||||
|
subtitle={$t('admin.machine_learning_availability_checks_description')}
|
||||||
|
>
|
||||||
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('admin.machine_learning_availability_checks_enabled')}
|
||||||
|
bind:checked={config.machineLearning.availabilityChecks.enabled}
|
||||||
|
disabled={disabled || !config.machineLearning.enabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
|
label={$t('admin.machine_learning_availability_checks_interval')}
|
||||||
|
bind:value={config.machineLearning.availabilityChecks.interval}
|
||||||
|
description={$t('admin.machine_learning_availability_checks_interval_description')}
|
||||||
|
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.availabilityChecks.enabled}
|
||||||
|
isEdited={config.machineLearning.availabilityChecks.interval !==
|
||||||
|
savedConfig.machineLearning.availabilityChecks.interval}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
|
label={$t('admin.machine_learning_availability_checks_timeout')}
|
||||||
|
bind:value={config.machineLearning.availabilityChecks.timeout}
|
||||||
|
description={$t('admin.machine_learning_availability_checks_timeout_description')}
|
||||||
|
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.availabilityChecks.enabled}
|
||||||
|
isEdited={config.machineLearning.availabilityChecks.timeout !==
|
||||||
|
savedConfig.machineLearning.availabilityChecks.timeout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="smart-search"
|
key="smart-search"
|
||||||
title={$t('admin.machine_learning_smart_search')}
|
title={$t('admin.machine_learning_smart_search')}
|
||||||
|
@@ -105,7 +105,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if inputType !== SettingInputFieldType.PASSWORD}
|
{#if inputType !== SettingInputFieldType.PASSWORD}
|
||||||
<div class="flex place-items-center place-content-center">
|
<div class="flex place-items-center place-content-center gap-2">
|
||||||
{#if inputType === SettingInputFieldType.COLOR}
|
{#if inputType === SettingInputFieldType.COLOR}
|
||||||
<input
|
<input
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
|
Reference in New Issue
Block a user