1
0
mirror of https://github.com/immich-app/immich.git synced 2025-01-27 17:28:09 +02:00

feat(web,server)!: configure machine learning via the UI (#3768)

This commit is contained in:
Jason Rasmussen 2023-08-25 00:15:03 -04:00 committed by GitHub
parent 2cccef174a
commit 8211afb726
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 831 additions and 649 deletions

View File

@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto {
*/ */
'total': number; 'total': number;
} }
/**
*
* @export
* @interface SearchConfigResponseDto
*/
export interface SearchConfigResponseDto {
/**
*
* @type {boolean}
* @memberof SearchConfigResponseDto
*/
'enabled': boolean;
}
/** /**
* *
* @export * @export
@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto {
* @type {boolean} * @type {boolean}
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'machineLearning': boolean; 'clipEncode': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'facialRecognition': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'search': boolean; 'search': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'sidecar': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'tagImage': boolean;
} }
/** /**
* *
@ -2611,6 +2616,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto * @memberof SystemConfigDto
*/ */
'job': SystemConfigJobDto; 'job': SystemConfigJobDto;
/**
*
* @type {SystemConfigMachineLearningDto}
* @memberof SystemConfigDto
*/
'machineLearning': SystemConfigMachineLearningDto;
/** /**
* *
* @type {SystemConfigOAuthDto} * @type {SystemConfigOAuthDto}
@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto {
*/ */
'videoConversion': JobSettingsDto; 'videoConversion': JobSettingsDto;
} }
/**
*
* @export
* @interface SystemConfigMachineLearningDto
*/
export interface SystemConfigMachineLearningDto {
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'clipEncodeEnabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'enabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'facialRecognitionEnabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'tagImageEnabled': boolean;
/**
*
* @type {string}
* @memberof SystemConfigMachineLearningDto
*/
'url': string;
}
/** /**
* *
* @export * @export
@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/search/config`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {string} [q] * @param {string} [q]
@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> {
return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {SearchApiSearchRequest} requestParameters Request parameters.
@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI {
return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SearchApi
*/
public getSearchConfig(options?: AxiosRequestConfig) {
return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {SearchApiSearchRequest} requestParameters Request parameters.

View File

@ -39,7 +39,7 @@ This often happens when using a reverse proxy or cloudflare tunnel in front of I
### Why is Immich slow on low-memory systems like the Raspberry Pi? ### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file. Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_ENABLED=false` in your .env file.
### How to disable machine-learning and TypeSense? ### How to disable machine-learning and TypeSense?
@ -47,7 +47,7 @@ Immich uses optional machine-learning features to enhance search results. This f
Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning. Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
::: :::
These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_URL=false` & `TYPESENSE_ENABLED=false` in your .env file. These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_ENABLED=false` & `TYPESENSE_ENABLED=false` in your .env file.
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)? ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?

View File

@ -132,7 +132,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
IMMICH_WEB_URL=http://immich-web:3000 IMMICH_WEB_URL=http://immich-web:3000
IMMICH_SERVER_URL=http://immich-server:3001 IMMICH_SERVER_URL=http://immich-server:3001
IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
#################################################################################### ####################################################################################
# Alternative API's External Address - Optional # Alternative API's External Address - Optional

View File

@ -51,10 +51,11 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## URLs ## URLs
| Variable | Description | Default | Services | | Variable | Description | Default | Services |
| :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- | | :-------------------------------- | :--------------------------- | :-----------------------------------: | :-------------------- |
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy | | `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy | | `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, set `"false"` to disable ML | `http://immich-machine-learning:3003` | server, microservices | | `IMMICH_MACHINE_LEARNING_ENABLED` | Enabled machine learning | `true` | server, microservices |
| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, | `http://immich-machine-learning:3003` | server, microservices |
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web | | `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web | | `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |

View File

@ -84,7 +84,6 @@ doc/SearchAlbumResponseDto.md
doc/SearchApi.md doc/SearchApi.md
doc/SearchAssetDto.md doc/SearchAssetDto.md
doc/SearchAssetResponseDto.md doc/SearchAssetResponseDto.md
doc/SearchConfigResponseDto.md
doc/SearchExploreItem.md doc/SearchExploreItem.md
doc/SearchExploreResponseDto.md doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md doc/SearchFacetCountResponseDto.md
@ -108,6 +107,7 @@ doc/SystemConfigApi.md
doc/SystemConfigDto.md doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md doc/SystemConfigFFmpegDto.md
doc/SystemConfigJobDto.md doc/SystemConfigJobDto.md
doc/SystemConfigMachineLearningDto.md
doc/SystemConfigOAuthDto.md doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.md doc/SystemConfigPasswordLoginDto.md
doc/SystemConfigStorageTemplateDto.md doc/SystemConfigStorageTemplateDto.md
@ -228,7 +228,6 @@ lib/model/queue_status_dto.dart
lib/model/search_album_response_dto.dart lib/model/search_album_response_dto.dart
lib/model/search_asset_dto.dart lib/model/search_asset_dto.dart
lib/model/search_asset_response_dto.dart lib/model/search_asset_response_dto.dart
lib/model/search_config_response_dto.dart
lib/model/search_explore_item.dart lib/model/search_explore_item.dart
lib/model/search_explore_response_dto.dart lib/model/search_explore_response_dto.dart
lib/model/search_facet_count_response_dto.dart lib/model/search_facet_count_response_dto.dart
@ -249,6 +248,7 @@ lib/model/smart_info_response_dto.dart
lib/model/system_config_dto.dart lib/model/system_config_dto.dart
lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_f_fmpeg_dto.dart
lib/model/system_config_job_dto.dart lib/model/system_config_job_dto.dart
lib/model/system_config_machine_learning_dto.dart
lib/model/system_config_o_auth_dto.dart lib/model/system_config_o_auth_dto.dart
lib/model/system_config_password_login_dto.dart lib/model/system_config_password_login_dto.dart
lib/model/system_config_storage_template_dto.dart lib/model/system_config_storage_template_dto.dart
@ -353,7 +353,6 @@ test/search_album_response_dto_test.dart
test/search_api_test.dart test/search_api_test.dart
test/search_asset_dto_test.dart test/search_asset_dto_test.dart
test/search_asset_response_dto_test.dart test/search_asset_response_dto_test.dart
test/search_config_response_dto_test.dart
test/search_explore_item_test.dart test/search_explore_item_test.dart
test/search_explore_response_dto_test.dart test/search_explore_response_dto_test.dart
test/search_facet_count_response_dto_test.dart test/search_facet_count_response_dto_test.dart
@ -377,6 +376,7 @@ test/system_config_api_test.dart
test/system_config_dto_test.dart test/system_config_dto_test.dart
test/system_config_f_fmpeg_dto_test.dart test/system_config_f_fmpeg_dto_test.dart
test/system_config_job_dto_test.dart test/system_config_job_dto_test.dart
test/system_config_machine_learning_dto_test.dart
test/system_config_o_auth_dto_test.dart test/system_config_o_auth_dto_test.dart
test/system_config_password_login_dto_test.dart test/system_config_password_login_dto_test.dart
test/system_config_storage_template_dto_test.dart test/system_config_storage_template_dto_test.dart

View File

@ -140,7 +140,6 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
@ -253,7 +252,6 @@ Class | Method | HTTP request | Description
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetDto](doc//SearchAssetDto.md) - [SearchAssetDto](doc//SearchAssetDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
- [SearchConfigResponseDto](doc//SearchConfigResponseDto.md)
- [SearchExploreItem](doc//SearchExploreItem.md) - [SearchExploreItem](doc//SearchExploreItem.md)
- [SearchExploreResponseDto](doc//SearchExploreResponseDto.md) - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md)
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
@ -274,6 +272,7 @@ Class | Method | HTTP request | Description
- [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)

View File

@ -10,7 +10,6 @@ All URIs are relative to */api*
Method | HTTP request | Description Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config |
[**search**](SearchApi.md#search) | **GET** /search | [**search**](SearchApi.md#search) | **GET** /search |
@ -65,57 +64,6 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getSearchConfig**
> SearchConfigResponseDto getSearchConfig()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
try {
final result = api_instance.getSearchConfig();
print(result);
} catch (e) {
print('Exception when calling SearchApi->getSearchConfig: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**SearchConfigResponseDto**](SearchConfigResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **search** # **search**
> SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion) > SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)

View File

@ -8,11 +8,14 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**machineLearning** | **bool** | | **clipEncode** | **bool** | |
**facialRecognition** | **bool** | |
**oauth** | **bool** | | **oauth** | **bool** | |
**oauthAutoLaunch** | **bool** | | **oauthAutoLaunch** | **bool** | |
**passwordLogin** | **bool** | | **passwordLogin** | **bool** | |
**search** | **bool** | | **search** | **bool** | |
**sidecar** | **bool** | |
**tagImage** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -10,6 +10,7 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | |
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | |
**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | |
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | | **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | |
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | | **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | |
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | |

View File

@ -1,4 +1,4 @@
# openapi.model.SearchConfigResponseDto # openapi.model.SystemConfigMachineLearningDto
## Load the model package ## Load the model package
```dart ```dart
@ -8,7 +8,11 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**clipEncodeEnabled** | **bool** | |
**enabled** | **bool** | | **enabled** | **bool** | |
**facialRecognitionEnabled** | **bool** | |
**tagImageEnabled** | **bool** | |
**url** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -115,7 +115,6 @@ part 'model/queue_status_dto.dart';
part 'model/search_album_response_dto.dart'; part 'model/search_album_response_dto.dart';
part 'model/search_asset_dto.dart'; part 'model/search_asset_dto.dart';
part 'model/search_asset_response_dto.dart'; part 'model/search_asset_response_dto.dart';
part 'model/search_config_response_dto.dart';
part 'model/search_explore_item.dart'; part 'model/search_explore_item.dart';
part 'model/search_explore_response_dto.dart'; part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart'; part 'model/search_facet_count_response_dto.dart';
@ -136,6 +135,7 @@ part 'model/smart_info_response_dto.dart';
part 'model/system_config_dto.dart'; part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_job_dto.dart'; part 'model/system_config_job_dto.dart';
part 'model/system_config_machine_learning_dto.dart';
part 'model/system_config_o_auth_dto.dart'; part 'model/system_config_o_auth_dto.dart';
part 'model/system_config_password_login_dto.dart'; part 'model/system_config_password_login_dto.dart';
part 'model/system_config_storage_template_dto.dart'; part 'model/system_config_storage_template_dto.dart';

View File

@ -60,47 +60,6 @@ class SearchApi {
return null; return null;
} }
/// Performs an HTTP 'GET /search/config' operation and returns the [Response].
Future<Response> getSearchConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/search/config';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<SearchConfigResponseDto?> getSearchConfig() async {
final response = await getSearchConfigWithHttpInfo();
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), 'SearchConfigResponseDto',) as SearchConfigResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /search' operation and returns the [Response]. /// Performs an HTTP 'GET /search' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@ -323,8 +323,6 @@ class ApiClient {
return SearchAssetDto.fromJson(value); return SearchAssetDto.fromJson(value);
case 'SearchAssetResponseDto': case 'SearchAssetResponseDto':
return SearchAssetResponseDto.fromJson(value); return SearchAssetResponseDto.fromJson(value);
case 'SearchConfigResponseDto':
return SearchConfigResponseDto.fromJson(value);
case 'SearchExploreItem': case 'SearchExploreItem':
return SearchExploreItem.fromJson(value); return SearchExploreItem.fromJson(value);
case 'SearchExploreResponseDto': case 'SearchExploreResponseDto':
@ -365,6 +363,8 @@ class ApiClient {
return SystemConfigFFmpegDto.fromJson(value); return SystemConfigFFmpegDto.fromJson(value);
case 'SystemConfigJobDto': case 'SystemConfigJobDto':
return SystemConfigJobDto.fromJson(value); return SystemConfigJobDto.fromJson(value);
case 'SystemConfigMachineLearningDto':
return SystemConfigMachineLearningDto.fromJson(value);
case 'SystemConfigOAuthDto': case 'SystemConfigOAuthDto':
return SystemConfigOAuthDto.fromJson(value); return SystemConfigOAuthDto.fromJson(value);
case 'SystemConfigPasswordLoginDto': case 'SystemConfigPasswordLoginDto':

View File

@ -1,98 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// 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 SearchConfigResponseDto {
/// Returns a new [SearchConfigResponseDto] instance.
SearchConfigResponseDto({
required this.enabled,
});
bool enabled;
@override
bool operator ==(Object other) => identical(this, other) || other is SearchConfigResponseDto &&
other.enabled == enabled;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode);
@override
String toString() => 'SearchConfigResponseDto[enabled=$enabled]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
return json;
}
/// Returns a new [SearchConfigResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SearchConfigResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SearchConfigResponseDto(
enabled: mapValueOfType<bool>(json, r'enabled')!,
);
}
return null;
}
static List<SearchConfigResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchConfigResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchConfigResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SearchConfigResponseDto> mapFromJson(dynamic json) {
final map = <String, SearchConfigResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SearchConfigResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SearchConfigResponseDto-objects as value to a dart map
static Map<String, List<SearchConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SearchConfigResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SearchConfigResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
};
}

View File

@ -13,14 +13,19 @@ part of openapi.api;
class ServerFeaturesDto { class ServerFeaturesDto {
/// Returns a new [ServerFeaturesDto] instance. /// Returns a new [ServerFeaturesDto] instance.
ServerFeaturesDto({ ServerFeaturesDto({
required this.machineLearning, required this.clipEncode,
required this.facialRecognition,
required this.oauth, required this.oauth,
required this.oauthAutoLaunch, required this.oauthAutoLaunch,
required this.passwordLogin, required this.passwordLogin,
required this.search, required this.search,
required this.sidecar,
required this.tagImage,
}); });
bool machineLearning; bool clipEncode;
bool facialRecognition;
bool oauth; bool oauth;
@ -30,33 +35,46 @@ class ServerFeaturesDto {
bool search; bool search;
bool sidecar;
bool tagImage;
@override @override
bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto && bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto &&
other.machineLearning == machineLearning && other.clipEncode == clipEncode &&
other.facialRecognition == facialRecognition &&
other.oauth == oauth && other.oauth == oauth &&
other.oauthAutoLaunch == oauthAutoLaunch && other.oauthAutoLaunch == oauthAutoLaunch &&
other.passwordLogin == passwordLogin && other.passwordLogin == passwordLogin &&
other.search == search; other.search == search &&
other.sidecar == sidecar &&
other.tagImage == tagImage;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(machineLearning.hashCode) + (clipEncode.hashCode) +
(facialRecognition.hashCode) +
(oauth.hashCode) + (oauth.hashCode) +
(oauthAutoLaunch.hashCode) + (oauthAutoLaunch.hashCode) +
(passwordLogin.hashCode) + (passwordLogin.hashCode) +
(search.hashCode); (search.hashCode) +
(sidecar.hashCode) +
(tagImage.hashCode);
@override @override
String toString() => 'ServerFeaturesDto[machineLearning=$machineLearning, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search]'; String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, facialRecognition=$facialRecognition, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'machineLearning'] = this.machineLearning; json[r'clipEncode'] = this.clipEncode;
json[r'facialRecognition'] = this.facialRecognition;
json[r'oauth'] = this.oauth; json[r'oauth'] = this.oauth;
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
json[r'passwordLogin'] = this.passwordLogin; json[r'passwordLogin'] = this.passwordLogin;
json[r'search'] = this.search; json[r'search'] = this.search;
json[r'sidecar'] = this.sidecar;
json[r'tagImage'] = this.tagImage;
return json; return json;
} }
@ -68,11 +86,14 @@ class ServerFeaturesDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return ServerFeaturesDto( return ServerFeaturesDto(
machineLearning: mapValueOfType<bool>(json, r'machineLearning')!, clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
oauth: mapValueOfType<bool>(json, r'oauth')!, oauth: mapValueOfType<bool>(json, r'oauth')!,
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!, oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!, passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
search: mapValueOfType<bool>(json, r'search')!, search: mapValueOfType<bool>(json, r'search')!,
sidecar: mapValueOfType<bool>(json, r'sidecar')!,
tagImage: mapValueOfType<bool>(json, r'tagImage')!,
); );
} }
return null; return null;
@ -120,11 +141,14 @@ class ServerFeaturesDto {
/// 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>{
'machineLearning', 'clipEncode',
'facialRecognition',
'oauth', 'oauth',
'oauthAutoLaunch', 'oauthAutoLaunch',
'passwordLogin', 'passwordLogin',
'search', 'search',
'sidecar',
'tagImage',
}; };
} }

View File

@ -15,6 +15,7 @@ class SystemConfigDto {
SystemConfigDto({ SystemConfigDto({
required this.ffmpeg, required this.ffmpeg,
required this.job, required this.job,
required this.machineLearning,
required this.oauth, required this.oauth,
required this.passwordLogin, required this.passwordLogin,
required this.storageTemplate, required this.storageTemplate,
@ -25,6 +26,8 @@ class SystemConfigDto {
SystemConfigJobDto job; SystemConfigJobDto job;
SystemConfigMachineLearningDto machineLearning;
SystemConfigOAuthDto oauth; SystemConfigOAuthDto oauth;
SystemConfigPasswordLoginDto passwordLogin; SystemConfigPasswordLoginDto passwordLogin;
@ -37,6 +40,7 @@ class SystemConfigDto {
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
other.ffmpeg == ffmpeg && other.ffmpeg == ffmpeg &&
other.job == job && other.job == job &&
other.machineLearning == machineLearning &&
other.oauth == oauth && other.oauth == oauth &&
other.passwordLogin == passwordLogin && other.passwordLogin == passwordLogin &&
other.storageTemplate == storageTemplate && other.storageTemplate == storageTemplate &&
@ -47,18 +51,20 @@ class SystemConfigDto {
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(ffmpeg.hashCode) + (ffmpeg.hashCode) +
(job.hashCode) + (job.hashCode) +
(machineLearning.hashCode) +
(oauth.hashCode) + (oauth.hashCode) +
(passwordLogin.hashCode) + (passwordLogin.hashCode) +
(storageTemplate.hashCode) + (storageTemplate.hashCode) +
(thumbnail.hashCode); (thumbnail.hashCode);
@override @override
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]'; String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'ffmpeg'] = this.ffmpeg; json[r'ffmpeg'] = this.ffmpeg;
json[r'job'] = this.job; json[r'job'] = this.job;
json[r'machineLearning'] = this.machineLearning;
json[r'oauth'] = this.oauth; json[r'oauth'] = this.oauth;
json[r'passwordLogin'] = this.passwordLogin; json[r'passwordLogin'] = this.passwordLogin;
json[r'storageTemplate'] = this.storageTemplate; json[r'storageTemplate'] = this.storageTemplate;
@ -76,6 +82,7 @@ class SystemConfigDto {
return SystemConfigDto( return SystemConfigDto(
ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
job: SystemConfigJobDto.fromJson(json[r'job'])!, job: SystemConfigJobDto.fromJson(json[r'job'])!,
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
@ -129,6 +136,7 @@ class SystemConfigDto {
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'ffmpeg', 'ffmpeg',
'job', 'job',
'machineLearning',
'oauth', 'oauth',
'passwordLogin', 'passwordLogin',
'storageTemplate', 'storageTemplate',

View File

@ -0,0 +1,130 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// 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 SystemConfigMachineLearningDto {
/// Returns a new [SystemConfigMachineLearningDto] instance.
SystemConfigMachineLearningDto({
required this.clipEncodeEnabled,
required this.enabled,
required this.facialRecognitionEnabled,
required this.tagImageEnabled,
required this.url,
});
bool clipEncodeEnabled;
bool enabled;
bool facialRecognitionEnabled;
bool tagImageEnabled;
String url;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigMachineLearningDto &&
other.clipEncodeEnabled == clipEncodeEnabled &&
other.enabled == enabled &&
other.facialRecognitionEnabled == facialRecognitionEnabled &&
other.tagImageEnabled == tagImageEnabled &&
other.url == url;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(clipEncodeEnabled.hashCode) +
(enabled.hashCode) +
(facialRecognitionEnabled.hashCode) +
(tagImageEnabled.hashCode) +
(url.hashCode);
@override
String toString() => 'SystemConfigMachineLearningDto[clipEncodeEnabled=$clipEncodeEnabled, enabled=$enabled, facialRecognitionEnabled=$facialRecognitionEnabled, tagImageEnabled=$tagImageEnabled, url=$url]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'clipEncodeEnabled'] = this.clipEncodeEnabled;
json[r'enabled'] = this.enabled;
json[r'facialRecognitionEnabled'] = this.facialRecognitionEnabled;
json[r'tagImageEnabled'] = this.tagImageEnabled;
json[r'url'] = this.url;
return json;
}
/// Returns a new [SystemConfigMachineLearningDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigMachineLearningDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SystemConfigMachineLearningDto(
clipEncodeEnabled: mapValueOfType<bool>(json, r'clipEncodeEnabled')!,
enabled: mapValueOfType<bool>(json, r'enabled')!,
facialRecognitionEnabled: mapValueOfType<bool>(json, r'facialRecognitionEnabled')!,
tagImageEnabled: mapValueOfType<bool>(json, r'tagImageEnabled')!,
url: mapValueOfType<String>(json, r'url')!,
);
}
return null;
}
static List<SystemConfigMachineLearningDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigMachineLearningDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigMachineLearningDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigMachineLearningDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigMachineLearningDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigMachineLearningDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigMachineLearningDto-objects as value to a dart map
static Map<String, List<SystemConfigMachineLearningDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigMachineLearningDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SystemConfigMachineLearningDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'clipEncodeEnabled',
'enabled',
'facialRecognitionEnabled',
'tagImageEnabled',
'url',
};
}

View File

@ -22,11 +22,6 @@ void main() {
// TODO // TODO
}); });
//Future<SearchConfigResponseDto> getSearchConfig() async
test('test getSearchConfig', () async {
// TODO
});
//Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
test('test search', () async { test('test search', () async {
// TODO // TODO

View File

@ -1,27 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// 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
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for SearchConfigResponseDto
void main() {
// final instance = SearchConfigResponseDto();
group('test SearchConfigResponseDto', () {
// bool enabled
test('to test the property `enabled`', () async {
// TODO
});
});
}

View File

@ -16,8 +16,13 @@ void main() {
// final instance = ServerFeaturesDto(); // final instance = ServerFeaturesDto();
group('test ServerFeaturesDto', () { group('test ServerFeaturesDto', () {
// bool machineLearning // bool clipEncode
test('to test the property `machineLearning`', () async { test('to test the property `clipEncode`', () async {
// TODO
});
// bool facialRecognition
test('to test the property `facialRecognition`', () async {
// TODO // TODO
}); });
@ -41,6 +46,16 @@ void main() {
// TODO // TODO
}); });
// bool sidecar
test('to test the property `sidecar`', () async {
// TODO
});
// bool tagImage
test('to test the property `tagImage`', () async {
// TODO
});
}); });

View File

@ -26,6 +26,11 @@ void main() {
// TODO // TODO
}); });
// SystemConfigMachineLearningDto machineLearning
test('to test the property `machineLearning`', () async {
// TODO
});
// SystemConfigOAuthDto oauth // SystemConfigOAuthDto oauth
test('to test the property `oauth`', () async { test('to test the property `oauth`', () async {
// TODO // TODO

View File

@ -0,0 +1,47 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// 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
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for SystemConfigMachineLearningDto
void main() {
// final instance = SystemConfigMachineLearningDto();
group('test SystemConfigMachineLearningDto', () {
// bool clipEncodeEnabled
test('to test the property `clipEncodeEnabled`', () async {
// TODO
});
// bool enabled
test('to test the property `enabled`', () async {
// TODO
});
// bool facialRecognitionEnabled
test('to test the property `facialRecognitionEnabled`', () async {
// TODO
});
// bool tagImageEnabled
test('to test the property `tagImageEnabled`', () async {
// TODO
});
// String url
test('to test the property `url`', () async {
// TODO
});
});
}

View File

@ -3243,38 +3243,6 @@
] ]
} }
}, },
"/search/config": {
"get": {
"operationId": "getSearchConfig",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchConfigResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/search/explore": { "/search/explore": {
"get": { "get": {
"operationId": "getExploreData", "operationId": "getExploreData",
@ -6424,17 +6392,6 @@
], ],
"type": "object" "type": "object"
}, },
"SearchConfigResponseDto": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"SearchExploreItem": { "SearchExploreItem": {
"properties": { "properties": {
"data": { "data": {
@ -6518,7 +6475,10 @@
}, },
"ServerFeaturesDto": { "ServerFeaturesDto": {
"properties": { "properties": {
"machineLearning": { "clipEncode": {
"type": "boolean"
},
"facialRecognition": {
"type": "boolean" "type": "boolean"
}, },
"oauth": { "oauth": {
@ -6532,11 +6492,20 @@
}, },
"search": { "search": {
"type": "boolean" "type": "boolean"
},
"sidecar": {
"type": "boolean"
},
"tagImage": {
"type": "boolean"
} }
}, },
"required": [ "required": [
"machineLearning", "clipEncode",
"facialRecognition",
"sidecar",
"search", "search",
"tagImage",
"oauth", "oauth",
"oauthAutoLaunch", "oauthAutoLaunch",
"passwordLogin" "passwordLogin"
@ -6868,6 +6837,9 @@
"job": { "job": {
"$ref": "#/components/schemas/SystemConfigJobDto" "$ref": "#/components/schemas/SystemConfigJobDto"
}, },
"machineLearning": {
"$ref": "#/components/schemas/SystemConfigMachineLearningDto"
},
"oauth": { "oauth": {
"$ref": "#/components/schemas/SystemConfigOAuthDto" "$ref": "#/components/schemas/SystemConfigOAuthDto"
}, },
@ -6883,6 +6855,7 @@
}, },
"required": [ "required": [
"ffmpeg", "ffmpeg",
"machineLearning",
"oauth", "oauth",
"passwordLogin", "passwordLogin",
"storageTemplate", "storageTemplate",
@ -6989,6 +6962,33 @@
], ],
"type": "object" "type": "object"
}, },
"SystemConfigMachineLearningDto": {
"properties": {
"clipEncodeEnabled": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"facialRecognitionEnabled": {
"type": "boolean"
},
"tagImageEnabled": {
"type": "boolean"
},
"url": {
"type": "string"
}
},
"required": [
"enabled",
"url",
"clipEncodeEnabled",
"facialRecognitionEnabled",
"tagImageEnabled"
],
"type": "object"
},
"SystemConfigOAuthDto": { "SystemConfigOAuthDto": {
"properties": { "properties": {
"autoLaunch": { "autoLaunch": {

View File

@ -1,5 +1,4 @@
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import { extname } from 'node:path'; import { extname } from 'node:path';
import pkg from 'src/../../package.json'; import pkg from 'src/../../package.json';
@ -24,17 +23,6 @@ export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${s
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
export const SEARCH_ENABLED = process.env.TYPESENSE_ENABLED !== 'false';
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';
export function assertMachineLearningEnabled() {
if (!MACHINE_LEARNING_ENABLED) {
throw new BadRequestException('Machine learning is not enabled.');
}
}
const image: Record<string, string[]> = { const image: Record<string, string[]> = {
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
'.ari': ['image/ari', 'image/x-arriflex-ari'], '.ari': ['image/ari', 'image/x-arriflex-ari'],

View File

@ -9,6 +9,7 @@ import {
newPersonRepositoryMock, newPersonRepositoryMock,
newSearchRepositoryMock, newSearchRepositoryMock,
newStorageRepositoryMock, newStorageRepositoryMock,
newSystemConfigRepositoryMock,
personStub, personStub,
} from '@test'; } from '@test';
import { IAssetRepository, WithoutProperty } from '../asset'; import { IAssetRepository, WithoutProperty } from '../asset';
@ -18,6 +19,7 @@ import { IPersonRepository } from '../person';
import { ISearchRepository } from '../search'; import { ISearchRepository } from '../search';
import { IMachineLearningRepository } from '../smart-info'; import { IMachineLearningRepository } from '../smart-info';
import { IStorageRepository } from '../storage'; import { IStorageRepository } from '../storage';
import { ISystemConfigRepository } from '../system-config';
import { IFaceRepository } from './face.repository'; import { IFaceRepository } from './face.repository';
import { FacialRecognitionService } from './facial-recognition.services'; import { FacialRecognitionService } from './facial-recognition.services';
@ -94,6 +96,7 @@ const faceSearch = {
describe(FacialRecognitionService.name, () => { describe(FacialRecognitionService.name, () => {
let sut: FacialRecognitionService; let sut: FacialRecognitionService;
let assetMock: jest.Mocked<IAssetRepository>; let assetMock: jest.Mocked<IAssetRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let faceMock: jest.Mocked<IFaceRepository>; let faceMock: jest.Mocked<IFaceRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let machineLearningMock: jest.Mocked<IMachineLearningRepository>; let machineLearningMock: jest.Mocked<IMachineLearningRepository>;
@ -104,6 +107,7 @@ describe(FacialRecognitionService.name, () => {
beforeEach(async () => { beforeEach(async () => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock();
faceMock = newFaceRepositoryMock(); faceMock = newFaceRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
machineLearningMock = newMachineLearningRepositoryMock(); machineLearningMock = newMachineLearningRepositoryMock();
@ -116,6 +120,7 @@ describe(FacialRecognitionService.name, () => {
sut = new FacialRecognitionService( sut = new FacialRecognitionService(
assetMock, assetMock,
configMock,
faceMock, faceMock,
jobMock, jobMock,
machineLearningMock, machineLearningMock,
@ -174,7 +179,7 @@ describe(FacialRecognitionService.name, () => {
machineLearningMock.detectFaces.mockResolvedValue([]); machineLearningMock.detectFaces.mockResolvedValue([]);
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleRecognizeFaces({ id: assetStub.image.id }); await sut.handleRecognizeFaces({ id: assetStub.image.id });
expect(machineLearningMock.detectFaces).toHaveBeenCalledWith({ expect(machineLearningMock.detectFaces).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
imagePath: assetStub.image.resizePath, imagePath: assetStub.image.resizePath,
}); });
expect(faceMock.create).not.toHaveBeenCalled(); expect(faceMock.create).not.toHaveBeenCalled();

View File

@ -1,7 +1,6 @@
import { Inject, Logger } from '@nestjs/common'; import { Inject, Logger } from '@nestjs/common';
import { join } from 'path'; import { join } from 'path';
import { IAssetRepository, WithoutProperty } from '../asset'; import { IAssetRepository, WithoutProperty } from '../asset';
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
import { usePagination } from '../domain.util'; import { usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media'; import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media';
@ -9,14 +8,17 @@ import { IPersonRepository } from '../person/person.repository';
import { ISearchRepository } from '../search/search.repository'; import { ISearchRepository } from '../search/search.repository';
import { IMachineLearningRepository } from '../smart-info'; import { IMachineLearningRepository } from '../smart-info';
import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
import { AssetFaceId, IFaceRepository } from './face.repository'; import { AssetFaceId, IFaceRepository } from './face.repository';
export class FacialRecognitionService { export class FacialRecognitionService {
private logger = new Logger(FacialRecognitionService.name); private logger = new Logger(FacialRecognitionService.name);
private storageCore = new StorageCore(); private storageCore = new StorageCore();
private configCore: SystemConfigCore;
constructor( constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IFaceRepository) private faceRepository: IFaceRepository, @Inject(IFaceRepository) private faceRepository: IFaceRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@ -24,9 +26,16 @@ export class FacialRecognitionService {
@Inject(IPersonRepository) private personRepository: IPersonRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {} ) {
this.configCore = new SystemConfigCore(configRepository);
}
async handleQueueRecognizeFaces({ force }: IBaseJob) { async handleQueueRecognizeFaces({ force }: IBaseJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
return true;
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination, { order: 'DESC' }) ? this.assetRepository.getAll(pagination, { order: 'DESC' })
@ -49,12 +58,17 @@ export class FacialRecognitionService {
} }
async handleRecognizeFaces({ id }: IEntityJob) { async handleRecognizeFaces({ id }: IEntityJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
return true;
}
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset || !MACHINE_LEARNING_ENABLED || !asset.resizePath) { if (!asset || !asset.resizePath) {
return false; return false;
} }
const faces = await this.machineLearning.detectFaces({ imagePath: asset.resizePath }); const faces = await this.machineLearning.detectFaces(machineLearning.url, { imagePath: asset.resizePath });
this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` }))); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` })));
@ -100,6 +114,11 @@ export class FacialRecognitionService {
} }
async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) { async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) {
return true;
}
const { assetId, personId, boundingBox, imageWidth, imageHeight } = data; const { assetId, personId, boundingBox, imageWidth, imageHeight } = data;
const [asset] = await this.assetRepository.getByIds([assetId]); const [asset] = await this.assetRepository.getByIds([assetId]);

View File

@ -2,8 +2,7 @@ import { AssetType } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { IAssetRepository, mapAsset } from '../asset'; import { IAssetRepository, mapAsset } from '../asset';
import { CommunicationEvent, ICommunicationRepository } from '../communication'; import { CommunicationEvent, ICommunicationRepository } from '../communication';
import { assertMachineLearningEnabled } from '../domain.constant'; import { FeatureFlag, ISystemConfigRepository } from '../system-config';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core'; import { SystemConfigCore } from '../system-config/system-config.core';
import { JobCommand, JobName, QueueName } from './job.constants'; import { JobCommand, JobName, QueueName } from './job.constants';
import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto';
@ -78,23 +77,25 @@ export class JobService {
return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
case QueueName.OBJECT_TAGGING: case QueueName.OBJECT_TAGGING:
assertMachineLearningEnabled(); await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE);
return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } });
case QueueName.CLIP_ENCODING: case QueueName.CLIP_ENCODING:
assertMachineLearningEnabled(); await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE);
return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } });
case QueueName.METADATA_EXTRACTION: case QueueName.METADATA_EXTRACTION:
return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
case QueueName.SIDECAR: case QueueName.SIDECAR:
await this.configCore.requireFeature(FeatureFlag.SIDECAR);
return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
case QueueName.THUMBNAIL_GENERATION: case QueueName.THUMBNAIL_GENERATION:
return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } });
case QueueName.RECOGNIZE_FACES: case QueueName.RECOGNIZE_FACES:
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } });
default: default:

View File

@ -1,3 +1,2 @@
export * from './search-config-response.dto';
export * from './search-explore.response.dto'; export * from './search-explore.response.dto';
export * from './search-response.dto'; export * from './search-response.dto';

View File

@ -1,3 +0,0 @@
export class SearchConfigResponseDto {
enabled!: boolean;
}

View File

@ -1,5 +1,3 @@
import { BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { import {
albumStub, albumStub,
assetStub, assetStub,
@ -12,12 +10,14 @@ import {
newJobRepositoryMock, newJobRepositoryMock,
newMachineLearningRepositoryMock, newMachineLearningRepositoryMock,
newSearchRepositoryMock, newSearchRepositoryMock,
newSystemConfigRepositoryMock,
searchStub, searchStub,
} from '@test'; } from '@test';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { IAlbumRepository } from '../album/album.repository'; import { IAlbumRepository } from '../album/album.repository';
import { IAssetRepository } from '../asset/asset.repository'; import { IAssetRepository } from '../asset/asset.repository';
import { IFaceRepository } from '../facial-recognition'; import { IFaceRepository } from '../facial-recognition';
import { ISystemConfigRepository } from '../index';
import { JobName } from '../job'; import { JobName } from '../job';
import { IJobRepository } from '../job/job.repository'; import { IJobRepository } from '../job/job.repository';
import { IMachineLearningRepository } from '../smart-info'; import { IMachineLearningRepository } from '../smart-info';
@ -31,29 +31,26 @@ describe(SearchService.name, () => {
let sut: SearchService; let sut: SearchService;
let albumMock: jest.Mocked<IAlbumRepository>; let albumMock: jest.Mocked<IAlbumRepository>;
let assetMock: jest.Mocked<IAssetRepository>; let assetMock: jest.Mocked<IAssetRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let faceMock: jest.Mocked<IFaceRepository>; let faceMock: jest.Mocked<IFaceRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let machineMock: jest.Mocked<IMachineLearningRepository>; let machineMock: jest.Mocked<IMachineLearningRepository>;
let searchMock: jest.Mocked<ISearchRepository>; let searchMock: jest.Mocked<ISearchRepository>;
let configMock: jest.Mocked<ConfigService>;
const makeSut = (value?: string) => { beforeEach(async () => {
if (value) {
configMock.get.mockReturnValue(value);
}
return new SearchService(albumMock, assetMock, faceMock, jobMock, machineMock, searchMock, configMock);
};
beforeEach(() => {
albumMock = newAlbumRepositoryMock(); albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock();
faceMock = newFaceRepositoryMock(); faceMock = newFaceRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
machineMock = newMachineLearningRepositoryMock(); machineMock = newMachineLearningRepositoryMock();
searchMock = newSearchRepositoryMock(); searchMock = newSearchRepositoryMock();
configMock = { get: jest.fn() } as unknown as jest.Mocked<ConfigService>;
sut = makeSut(); sut = new SearchService(albumMock, assetMock, configMock, faceMock, jobMock, machineMock, searchMock);
searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false });
await sut.init();
}); });
afterEach(() => { afterEach(() => {
@ -86,45 +83,18 @@ describe(SearchService.name, () => {
}); });
}); });
describe('isEnabled', () => {
it('should be enabled by default', () => {
expect(sut.isEnabled()).toBe(true);
});
it('should be disabled via an env variable', () => {
const sut = makeSut('false');
expect(sut.isEnabled()).toBe(false);
});
});
describe('getConfig', () => {
it('should return the config', () => {
expect(sut.getConfig()).toEqual({ enabled: true });
});
it('should return the config when search is disabled', () => {
const sut = makeSut('false');
expect(sut.getConfig()).toEqual({ enabled: false });
});
});
describe(`init`, () => { describe(`init`, () => {
it('should skip when search is disabled', async () => { // it('should skip when search is disabled', async () => {
const sut = makeSut('false'); // await sut.init();
await sut.init(); // expect(searchMock.setup).not.toHaveBeenCalled();
// expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled();
// expect(jobMock.queue).not.toHaveBeenCalled();
expect(searchMock.setup).not.toHaveBeenCalled(); // sut.teardown();
expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled(); // });
expect(jobMock.queue).not.toHaveBeenCalled();
sut.teardown();
});
it('should skip schema migration if not needed', async () => { it('should skip schema migration if not needed', async () => {
searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false });
await sut.init(); await sut.init();
expect(searchMock.setup).toHaveBeenCalled(); expect(searchMock.setup).toHaveBeenCalled();
@ -145,14 +115,14 @@ describe(SearchService.name, () => {
}); });
describe('search', () => { describe('search', () => {
it('should throw an error is search is disabled', async () => { // it('should throw an error is search is disabled', async () => {
const sut = makeSut('false'); // sut['enabled'] = false;
await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException); // await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
expect(searchMock.searchAlbums).not.toHaveBeenCalled(); // expect(searchMock.searchAlbums).not.toHaveBeenCalled();
expect(searchMock.searchAssets).not.toHaveBeenCalled(); // expect(searchMock.searchAssets).not.toHaveBeenCalled();
}); // });
it('should search assets and albums', async () => { it('should search assets and albums', async () => {
searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults); searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults);
@ -205,7 +175,7 @@ describe(SearchService.name, () => {
}); });
it('should skip if search is disabled', async () => { it('should skip if search is disabled', async () => {
const sut = makeSut('false'); sut['enabled'] = false;
await sut.handleIndexAssets(); await sut.handleIndexAssets();
@ -216,7 +186,7 @@ describe(SearchService.name, () => {
describe('handleIndexAsset', () => { describe('handleIndexAsset', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleIndexAsset({ ids: [assetStub.image.id] }); sut.handleIndexAsset({ ids: [assetStub.image.id] });
}); });
@ -227,7 +197,7 @@ describe(SearchService.name, () => {
describe('handleIndexAlbums', () => { describe('handleIndexAlbums', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleIndexAlbums(); sut.handleIndexAlbums();
}); });
@ -242,7 +212,7 @@ describe(SearchService.name, () => {
describe('handleIndexAlbum', () => { describe('handleIndexAlbum', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleIndexAlbum({ ids: [albumStub.empty.id] }); sut.handleIndexAlbum({ ids: [albumStub.empty.id] });
}); });
@ -253,7 +223,7 @@ describe(SearchService.name, () => {
describe('handleRemoveAlbum', () => { describe('handleRemoveAlbum', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleRemoveAlbum({ ids: ['album1'] }); sut.handleRemoveAlbum({ ids: ['album1'] });
}); });
@ -264,7 +234,7 @@ describe(SearchService.name, () => {
describe('handleRemoveAsset', () => { describe('handleRemoveAsset', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleRemoveAsset({ ids: ['asset1'] }); sut.handleRemoveAsset({ ids: ['asset1'] });
}); });
@ -305,7 +275,7 @@ describe(SearchService.name, () => {
}); });
it('should skip if search is disabled', async () => { it('should skip if search is disabled', async () => {
const sut = makeSut('false'); sut['enabled'] = false;
await sut.handleIndexFaces(); await sut.handleIndexFaces();
@ -315,7 +285,7 @@ describe(SearchService.name, () => {
describe('handleIndexAsset', () => { describe('handleIndexAsset', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' }); sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' });
expect(searchMock.importFaces).not.toHaveBeenCalled(); expect(searchMock.importFaces).not.toHaveBeenCalled();
@ -333,7 +303,7 @@ describe(SearchService.name, () => {
describe('handleRemoveFace', () => { describe('handleRemoveFace', () => {
it('should skip if search is disabled', () => { it('should skip if search is disabled', () => {
const sut = makeSut('false'); sut['enabled'] = false;
sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' }); sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' });
}); });

View File

@ -1,18 +1,17 @@
import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities'; import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { mapAlbumWithAssets } from '../album'; import { mapAlbumWithAssets } from '../album';
import { IAlbumRepository } from '../album/album.repository'; import { IAlbumRepository } from '../album/album.repository';
import { AssetResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, mapAsset } from '../asset';
import { IAssetRepository } from '../asset/asset.repository'; import { IAssetRepository } from '../asset/asset.repository';
import { AuthUserDto } from '../auth'; import { AuthUserDto } from '../auth';
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
import { usePagination } from '../domain.util'; import { usePagination } from '../domain.util';
import { AssetFaceId, IFaceRepository } from '../facial-recognition'; import { AssetFaceId, IFaceRepository } from '../facial-recognition';
import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
import { IMachineLearningRepository } from '../smart-info'; import { IMachineLearningRepository } from '../smart-info';
import { FeatureFlag, ISystemConfigRepository, SystemConfigCore } from '../system-config';
import { SearchDto } from './dto'; import { SearchDto } from './dto';
import { SearchConfigResponseDto, SearchResponseDto } from './response-dto'; import { SearchResponseDto } from './response-dto';
import { import {
ISearchRepository, ISearchRepository,
OwnedFaceEntity, OwnedFaceEntity,
@ -30,8 +29,9 @@ interface SyncQueue {
@Injectable() @Injectable()
export class SearchService { export class SearchService {
private logger = new Logger(SearchService.name); private logger = new Logger(SearchService.name);
private enabled: boolean; private enabled = false;
private timer: NodeJS.Timer | null = null; private timer: NodeJS.Timer | null = null;
private configCore: SystemConfigCore;
private albumQueue: SyncQueue = { private albumQueue: SyncQueue = {
upsert: new Set(), upsert: new Set(),
@ -51,16 +51,13 @@ export class SearchService {
constructor( constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IFaceRepository) private faceRepository: IFaceRepository, @Inject(IFaceRepository) private faceRepository: IFaceRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(ISearchRepository) private searchRepository: ISearchRepository,
configService: ConfigService,
) { ) {
this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false'; this.configCore = new SystemConfigCore(configRepository);
if (this.enabled) {
this.timer = setInterval(() => this.flush(), 5_000);
}
} }
teardown() { teardown() {
@ -70,17 +67,8 @@ export class SearchService {
} }
} }
isEnabled() {
return this.enabled;
}
getConfig(): SearchConfigResponseDto {
return {
enabled: this.enabled,
};
}
async init() { async init() {
this.enabled = await this.configCore.hasFeature(FeatureFlag.SEARCH);
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
@ -101,10 +89,13 @@ export class SearchService {
this.logger.debug('Queueing job to re-index all faces'); this.logger.debug('Queueing job to re-index all faces');
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES });
} }
this.timer = setInterval(() => this.flush(), 5_000);
} }
async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> { async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
this.assertEnabled(); await this.configCore.requireFeature(FeatureFlag.SEARCH);
const results = await this.searchRepository.explore(authUser.id); const results = await this.searchRepository.explore(authUser.id);
const lookup = await this.getLookupMap( const lookup = await this.getLookupMap(
results.reduce( results.reduce(
@ -126,16 +117,18 @@ export class SearchService {
} }
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> { async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
this.assertEnabled(); const { machineLearning } = await this.configCore.getConfig();
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const query = dto.q || dto.query || '*'; const query = dto.q || dto.query || '*';
const strategy = dto.clip && MACHINE_LEARNING_ENABLED ? SearchStrategy.CLIP : SearchStrategy.TEXT; const hasClip = machineLearning.enabled && machineLearning.clipEncodeEnabled;
const strategy = dto.clip && hasClip ? SearchStrategy.CLIP : SearchStrategy.TEXT;
const filters = { userId: authUser.id, ...dto }; const filters = { userId: authUser.id, ...dto };
let assets: SearchResult<AssetEntity>; let assets: SearchResult<AssetEntity>;
switch (strategy) { switch (strategy) {
case SearchStrategy.CLIP: case SearchStrategy.CLIP:
const clip = await this.machineLearning.encodeText(query); const clip = await this.machineLearning.encodeText(machineLearning.url, query);
assets = await this.searchRepository.vectorSearch(clip, filters); assets = await this.searchRepository.vectorSearch(clip, filters);
break; break;
case SearchStrategy.TEXT: case SearchStrategy.TEXT:
@ -333,12 +326,6 @@ export class SearchService {
} }
} }
private assertEnabled() {
if (!this.enabled) {
throw new BadRequestException('Search is disabled');
}
}
private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> { private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> {
const entities = await this.albumRepository.getByIds(ids); const entities = await this.albumRepository.getByIds(ids);
return this.patchAlbums(entities); return this.patchAlbums(entities);

View File

@ -1,4 +1,4 @@
import { IServerVersion } from '@app/domain'; import { FeatureFlags, IServerVersion } from '@app/domain';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
export class ServerPingResponse { export class ServerPingResponse {
@ -79,10 +79,14 @@ export class ServerMediaTypesResponseDto {
sidecar!: string[]; sidecar!: string[];
} }
export class ServerFeaturesDto { export class ServerFeaturesDto implements FeatureFlags {
machineLearning!: boolean; clipEncode!: boolean;
facialRecognition!: boolean;
sidecar!: boolean;
search!: boolean; search!: boolean;
tagImage!: boolean;
// TODO: use these instead of `POST oauth/config`
oauth!: boolean; oauth!: boolean;
oauthAutoLaunch!: boolean; oauthAutoLaunch!: boolean;
passwordLogin!: boolean; passwordLogin!: boolean;

View File

@ -147,11 +147,14 @@ describe(ServerInfoService.name, () => {
describe('getFeatures', () => { describe('getFeatures', () => {
it('should respond the server features', async () => { it('should respond the server features', async () => {
await expect(sut.getFeatures()).resolves.toEqual({ await expect(sut.getFeatures()).resolves.toEqual({
machineLearning: true, clipEncode: true,
facialRecognition: true,
oauth: false, oauth: false,
oauthAutoLaunch: false, oauthAutoLaunch: false,
passwordLogin: true, passwordLogin: true,
search: true, search: true,
sidecar: true,
tagImage: true,
}); });
expect(configMock.load).toHaveBeenCalled(); expect(configMock.load).toHaveBeenCalled();
}); });

View File

@ -1,9 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { MACHINE_LEARNING_ENABLED, mimeTypes, SEARCH_ENABLED, serverVersion } from '../domain.constant'; import { mimeTypes, serverVersion } from '../domain.constant';
import { asHumanReadable } from '../domain.util'; import { asHumanReadable } from '../domain.util';
import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { ISystemConfigRepository } from '../system-config'; import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { IUserRepository, UserStatsQueryResponse } from '../user'; import { IUserRepository, UserStatsQueryResponse } from '../user';
import { import {
ServerFeaturesDto, ServerFeaturesDto,
@ -52,18 +51,8 @@ export class ServerInfoService {
return serverVersion; return serverVersion;
} }
async getFeatures(): Promise<ServerFeaturesDto> { getFeatures(): Promise<ServerFeaturesDto> {
const config = await this.configCore.getConfig(); return this.configCore.getFeatures();
return {
machineLearning: MACHINE_LEARNING_ENABLED,
search: SEARCH_ENABLED,
// TODO: use these instead of `POST oauth/config`
oauth: config.oauth.enabled,
oauthAutoLaunch: config.oauth.autoLaunch,
passwordLogin: config.passwordLogin.enabled,
};
} }
async getStats(): Promise<ServerStatsResponseDto> { async getStats(): Promise<ServerStatsResponseDto> {

View File

@ -20,8 +20,8 @@ export interface DetectFaceResult {
} }
export interface IMachineLearningRepository { export interface IMachineLearningRepository {
classifyImage(input: MachineLearningInput): Promise<string[]>; classifyImage(url: string, input: MachineLearningInput): Promise<string[]>;
encodeImage(input: MachineLearningInput): Promise<number[]>; encodeImage(url: string, input: MachineLearningInput): Promise<number[]>;
encodeText(input: string): Promise<number[]>; encodeText(url: string, input: string): Promise<number[]>;
detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]>; detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]>;
} }

View File

@ -5,9 +5,11 @@ import {
newJobRepositoryMock, newJobRepositoryMock,
newMachineLearningRepositoryMock, newMachineLearningRepositoryMock,
newSmartInfoRepositoryMock, newSmartInfoRepositoryMock,
newSystemConfigRepositoryMock,
} from '@test'; } from '@test';
import { IAssetRepository, WithoutProperty } from '../asset'; import { IAssetRepository, WithoutProperty } from '../asset';
import { IJobRepository, JobName } from '../job'; import { IJobRepository, JobName } from '../job';
import { ISystemConfigRepository } from '../system-config';
import { IMachineLearningRepository } from './machine-learning.interface'; import { IMachineLearningRepository } from './machine-learning.interface';
import { ISmartInfoRepository } from './smart-info.repository'; import { ISmartInfoRepository } from './smart-info.repository';
import { SmartInfoService } from './smart-info.service'; import { SmartInfoService } from './smart-info.service';
@ -20,16 +22,18 @@ const asset = {
describe(SmartInfoService.name, () => { describe(SmartInfoService.name, () => {
let sut: SmartInfoService; let sut: SmartInfoService;
let assetMock: jest.Mocked<IAssetRepository>; let assetMock: jest.Mocked<IAssetRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let smartMock: jest.Mocked<ISmartInfoRepository>; let smartMock: jest.Mocked<ISmartInfoRepository>;
let machineMock: jest.Mocked<IMachineLearningRepository>; let machineMock: jest.Mocked<IMachineLearningRepository>;
beforeEach(async () => { beforeEach(async () => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock();
smartMock = newSmartInfoRepositoryMock(); smartMock = newSmartInfoRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
machineMock = newMachineLearningRepositoryMock(); machineMock = newMachineLearningRepositoryMock();
sut = new SmartInfoService(assetMock, jobMock, smartMock, machineMock); sut = new SmartInfoService(assetMock, configMock, jobMock, smartMock, machineMock);
assetMock.getByIds.mockResolvedValue([asset]); assetMock.getByIds.mockResolvedValue([asset]);
}); });
@ -80,7 +84,9 @@ describe(SmartInfoService.name, () => {
await sut.handleClassifyImage({ id: asset.id }); await sut.handleClassifyImage({ id: asset.id });
expect(machineMock.classifyImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' }); expect(machineMock.classifyImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
imagePath: 'path/to/resize.ext',
});
expect(smartMock.upsert).toHaveBeenCalledWith({ expect(smartMock.upsert).toHaveBeenCalledWith({
assetId: 'asset-1', assetId: 'asset-1',
tags: ['tag1', 'tag2', 'tag3'], tags: ['tag1', 'tag2', 'tag3'],
@ -139,7 +145,9 @@ describe(SmartInfoService.name, () => {
await sut.handleEncodeClip({ id: asset.id }); await sut.handleEncodeClip({ id: asset.id });
expect(machineMock.encodeImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' }); expect(machineMock.encodeImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', {
imagePath: 'path/to/resize.ext',
});
expect(smartMock.upsert).toHaveBeenCalledWith({ expect(smartMock.upsert).toHaveBeenCalledWith({
assetId: 'asset-1', assetId: 'asset-1',
clipEmbedding: [0.01, 0.02, 0.03], clipEmbedding: [0.01, 0.02, 0.03],

View File

@ -1,23 +1,31 @@
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IAssetRepository, WithoutProperty } from '../asset'; import { IAssetRepository, WithoutProperty } from '../asset';
import { MACHINE_LEARNING_ENABLED } from '../domain.constant';
import { usePagination } from '../domain.util'; import { usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job';
import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
import { IMachineLearningRepository } from './machine-learning.interface'; import { IMachineLearningRepository } from './machine-learning.interface';
import { ISmartInfoRepository } from './smart-info.repository'; import { ISmartInfoRepository } from './smart-info.repository';
@Injectable() @Injectable()
export class SmartInfoService { export class SmartInfoService {
private logger = new Logger(SmartInfoService.name); private configCore: SystemConfigCore;
constructor( constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
) {} ) {
this.configCore = new SystemConfigCore(configRepository);
}
async handleQueueObjectTagging({ force }: IBaseJob) { async handleQueueObjectTagging({ force }: IBaseJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.tagImageEnabled) {
return true;
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination)
@ -34,19 +42,28 @@ export class SmartInfoService {
} }
async handleClassifyImage({ id }: IEntityJob) { async handleClassifyImage({ id }: IEntityJob) {
const [asset] = await this.assetRepository.getByIds([id]); const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.tagImageEnabled) {
return true;
}
if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) { const [asset] = await this.assetRepository.getByIds([id]);
if (!asset.resizePath) {
return false; return false;
} }
const tags = await this.machineLearning.classifyImage({ imagePath: asset.resizePath }); const tags = await this.machineLearning.classifyImage(machineLearning.url, { imagePath: asset.resizePath });
await this.repository.upsert({ assetId: asset.id, tags }); await this.repository.upsert({ assetId: asset.id, tags });
return true; return true;
} }
async handleQueueEncodeClip({ force }: IBaseJob) { async handleQueueEncodeClip({ force }: IBaseJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) {
return true;
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination)
@ -63,13 +80,17 @@ export class SmartInfoService {
} }
async handleEncodeClip({ id }: IEntityJob) { async handleEncodeClip({ id }: IEntityJob) {
const [asset] = await this.assetRepository.getByIds([id]); const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) {
return true;
}
if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) { const [asset] = await this.assetRepository.getByIds([id]);
if (!asset.resizePath) {
return false; return false;
} }
const clipEmbedding = await this.machineLearning.encodeImage({ imagePath: asset.resizePath }); const clipEmbedding = await this.machineLearning.encodeImage(machineLearning.url, { imagePath: asset.resizePath });
await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding }); await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding });
return true; return true;

View File

@ -0,0 +1,19 @@
import { IsBoolean, IsUrl, ValidateIf } from 'class-validator';
export class SystemConfigMachineLearningDto {
@IsBoolean()
enabled!: boolean;
@IsUrl({ require_tld: false })
@ValidateIf((dto) => dto.enabled)
url!: string;
@IsBoolean()
clipEncodeEnabled!: boolean;
@IsBoolean()
facialRecognitionEnabled!: boolean;
@IsBoolean()
tagImageEnabled!: boolean;
}

View File

@ -4,16 +4,22 @@ import { Type } from 'class-transformer';
import { IsObject, ValidateNested } from 'class-validator'; import { IsObject, ValidateNested } from 'class-validator';
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
import { SystemConfigJobDto } from './system-config-job.dto'; import { SystemConfigJobDto } from './system-config-job.dto';
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
import { SystemConfigOAuthDto } from './system-config-oauth.dto'; import { SystemConfigOAuthDto } from './system-config-oauth.dto';
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
export class SystemConfigDto { export class SystemConfigDto implements SystemConfig {
@Type(() => SystemConfigFFmpegDto) @Type(() => SystemConfigFFmpegDto)
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
ffmpeg!: SystemConfigFFmpegDto; ffmpeg!: SystemConfigFFmpegDto;
@Type(() => SystemConfigMachineLearningDto)
@ValidateNested()
@IsObject()
machineLearning!: SystemConfigMachineLearningDto;
@Type(() => SystemConfigOAuthDto) @Type(() => SystemConfigOAuthDto)
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()

View File

@ -1,5 +1,6 @@
export * from './dto'; export * from './dto';
export * from './response-dto'; export * from './response-dto';
export * from './system-config.constants'; export * from './system-config.constants';
export * from './system-config.core';
export * from './system-config.repository'; export * from './system-config.repository';
export * from './system-config.service'; export * from './system-config.service';

View File

@ -9,7 +9,7 @@ import {
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
} from '@app/infra/entities'; } from '@app/infra/entities';
import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
@ -44,6 +44,13 @@ export const defaults = Object.freeze<SystemConfig>({
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, [QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
}, },
machineLearning: {
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
facialRecognitionEnabled: true,
tagImageEnabled: true,
clipEncodeEnabled: true,
},
oauth: { oauth: {
enabled: false, enabled: false,
issuerUrl: '', issuerUrl: '',
@ -71,6 +78,19 @@ export const defaults = Object.freeze<SystemConfig>({
}, },
}); });
export enum FeatureFlag {
CLIP_ENCODE = 'clipEncode',
FACIAL_RECOGNITION = 'facialRecognition',
TAG_IMAGE = 'tagImage',
SIDECAR = 'sidecar',
SEARCH = 'search',
OAUTH = 'oauth',
OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
PASSWORD_LOGIN = 'passwordLogin',
}
export type FeatureFlags = Record<FeatureFlag, boolean>;
const singleton = new Subject<SystemConfig>(); const singleton = new Subject<SystemConfig>();
@Injectable() @Injectable()
@ -82,6 +102,53 @@ export class SystemConfigCore {
constructor(private repository: ISystemConfigRepository) {} constructor(private repository: ISystemConfigRepository) {}
async requireFeature(feature: FeatureFlag) {
const hasFeature = await this.hasFeature(feature);
if (!hasFeature) {
switch (feature) {
case FeatureFlag.CLIP_ENCODE:
throw new BadRequestException('Clip encoding is not enabled');
case FeatureFlag.FACIAL_RECOGNITION:
throw new BadRequestException('Facial recognition is not enabled');
case FeatureFlag.TAG_IMAGE:
throw new BadRequestException('Image tagging is not enabled');
case FeatureFlag.SIDECAR:
throw new BadRequestException('Sidecar is not enabled');
case FeatureFlag.SEARCH:
throw new BadRequestException('Search is not enabled');
case FeatureFlag.OAUTH:
throw new BadRequestException('OAuth is not enabled');
case FeatureFlag.PASSWORD_LOGIN:
throw new BadRequestException('Password login is not enabled');
default:
throw new ForbiddenException(`Missing required feature: ${feature}`);
}
}
}
async hasFeature(feature: FeatureFlag) {
const features = await this.getFeatures();
return features[feature] ?? false;
}
async getFeatures(): Promise<FeatureFlags> {
const config = await this.getConfig();
const mlEnabled = config.machineLearning.enabled;
return {
[FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clipEncodeEnabled,
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognitionEnabled,
[FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.tagImageEnabled,
[FeatureFlag.SIDECAR]: true,
[FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
// TODO: use these instead of `POST oauth/config`
[FeatureFlag.OAUTH]: config.oauth.enabled,
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
};
}
public getDefaults(): SystemConfig { public getDefaults(): SystemConfig {
return defaults; return defaults;
} }

View File

@ -46,6 +46,13 @@ const updatedConfig = Object.freeze<SystemConfig>({
accel: TranscodeHWAccel.DISABLED, accel: TranscodeHWAccel.DISABLED,
tonemap: ToneMapping.HABLE, tonemap: ToneMapping.HABLE,
}, },
machineLearning: {
enabled: true,
url: 'http://immich-machine-learning:3003',
facialRecognitionEnabled: true,
tagImageEnabled: true,
clipEncodeEnabled: true,
},
oauth: { oauth: {
autoLaunch: true, autoLaunch: true,
autoRegister: true, autoRegister: true,

View File

@ -1,4 +1,4 @@
import { JobService, MACHINE_LEARNING_ENABLED, SearchService, StorageService } from '@app/domain'; import { JobService, SearchService, ServerInfoService, StorageService } from '@app/domain';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule'; import { Cron, CronExpression } from '@nestjs/schedule';
@ -10,6 +10,7 @@ export class AppService {
private jobService: JobService, private jobService: JobService,
private searchService: SearchService, private searchService: SearchService,
private storageService: StorageService, private storageService: StorageService,
private serverService: ServerInfoService,
) {} ) {}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
@ -20,8 +21,6 @@ export class AppService {
async init() { async init() {
this.storageService.init(); this.storageService.init();
await this.searchService.init(); await this.searchService.init();
this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`);
this.logger.log(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`);
this.logger.log(`Search is ${this.searchService.isEnabled() ? 'enabled' : 'disabled'}`);
} }
} }

View File

@ -1,11 +1,4 @@
import { import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain';
AuthUserDto,
SearchConfigResponseDto,
SearchDto,
SearchExploreResponseDto,
SearchResponseDto,
SearchService,
} from '@app/domain';
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Authenticated, AuthUser } from '../app.guard'; import { Authenticated, AuthUser } from '../app.guard';
@ -23,11 +16,6 @@ export class SearchController {
return this.service.search(authUser, dto); return this.service.search(authUser, dto);
} }
@Get('config')
getSearchConfig(): SearchConfigResponseDto {
return this.service.getConfig();
}
@Get('explore') @Get('explore')
getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> { getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>; return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>;

View File

@ -37,6 +37,12 @@ export enum SystemConfigKey {
JOB_SEARCH_CONCURRENCY = 'job.search.concurrency', JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency', JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
MACHINE_LEARNING_ENABLED = 'machineLearning.enabled',
MACHINE_LEARNING_URL = 'machineLearning.url',
MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED = 'machineLearning.facialRecognitionEnabled',
MACHINE_LEARNING_TAG_IMAGE_ENABLED = 'machineLearning.tagImageEnabled',
MACHINE_LEARNING_CLIP_ENCODE_ENABLED = 'machineLearning.clipEncodeEnabled',
OAUTH_ENABLED = 'oauth.enabled', OAUTH_ENABLED = 'oauth.enabled',
OAUTH_ISSUER_URL = 'oauth.issuerUrl', OAUTH_ISSUER_URL = 'oauth.issuerUrl',
OAUTH_CLIENT_ID = 'oauth.clientId', OAUTH_CLIENT_ID = 'oauth.clientId',
@ -105,6 +111,13 @@ export interface SystemConfig {
tonemap: ToneMapping; tonemap: ToneMapping;
}; };
job: Record<QueueName, { concurrency: number }>; job: Record<QueueName, { concurrency: number }>;
machineLearning: {
enabled: boolean;
url: string;
clipEncodeEnabled: boolean;
facialRecognitionEnabled: boolean;
tagImageEnabled: boolean;
};
oauth: { oauth: {
enabled: boolean; enabled: boolean;
issuerUrl: string; issuerUrl: string;

View File

@ -1,9 +1,9 @@
import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput, MACHINE_LEARNING_URL } from '@app/domain'; import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput } from '@app/domain';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import axios from 'axios'; import axios from 'axios';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
const client = axios.create({ baseURL: MACHINE_LEARNING_URL }); const client = axios.create();
@Injectable() @Injectable()
export class MachineLearningRepository implements IMachineLearningRepository { export class MachineLearningRepository implements IMachineLearningRepository {
@ -11,19 +11,19 @@ export class MachineLearningRepository implements IMachineLearningRepository {
return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data); return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data);
} }
classifyImage(input: MachineLearningInput): Promise<string[]> { classifyImage(url: string, input: MachineLearningInput): Promise<string[]> {
return this.post<string[]>(input, '/image-classifier/tag-image'); return this.post<string[]>(input, `${url}/image-classifier/tag-image`);
} }
detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]> { detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]> {
return this.post<DetectFaceResult[]>(input, '/facial-recognition/detect-faces'); return this.post<DetectFaceResult[]>(input, `${url}/facial-recognition/detect-faces`);
} }
encodeImage(input: MachineLearningInput): Promise<number[]> { encodeImage(url: string, input: MachineLearningInput): Promise<number[]> {
return this.post<number[]>(input, '/sentence-transformer/encode-image'); return this.post<number[]>(input, `${url}/sentence-transformer/encode-image`);
} }
encodeText(input: string): Promise<number[]> { encodeText(url: string, input: string): Promise<number[]> {
return client.post<number[]>('/sentence-transformer/encode-text', { text: input }).then((res) => res.data); return client.post<number[]>(`${url}/sentence-transformer/encode-text`, { text: input }).then((res) => res.data);
} }
} }

View File

@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto {
*/ */
'total': number; 'total': number;
} }
/**
*
* @export
* @interface SearchConfigResponseDto
*/
export interface SearchConfigResponseDto {
/**
*
* @type {boolean}
* @memberof SearchConfigResponseDto
*/
'enabled': boolean;
}
/** /**
* *
* @export * @export
@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto {
* @type {boolean} * @type {boolean}
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'machineLearning': boolean; 'clipEncode': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'facialRecognition': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'search': boolean; 'search': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'sidecar': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'tagImage': boolean;
} }
/** /**
* *
@ -2611,6 +2616,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto * @memberof SystemConfigDto
*/ */
'job': SystemConfigJobDto; 'job': SystemConfigJobDto;
/**
*
* @type {SystemConfigMachineLearningDto}
* @memberof SystemConfigDto
*/
'machineLearning': SystemConfigMachineLearningDto;
/** /**
* *
* @type {SystemConfigOAuthDto} * @type {SystemConfigOAuthDto}
@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto {
*/ */
'videoConversion': JobSettingsDto; 'videoConversion': JobSettingsDto;
} }
/**
*
* @export
* @interface SystemConfigMachineLearningDto
*/
export interface SystemConfigMachineLearningDto {
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'clipEncodeEnabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'enabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'facialRecognitionEnabled': boolean;
/**
*
* @type {boolean}
* @memberof SystemConfigMachineLearningDto
*/
'tagImageEnabled': boolean;
/**
*
* @type {string}
* @memberof SystemConfigMachineLearningDto
*/
'url': string;
}
/** /**
* *
* @export * @export
@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/search/config`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {string} [q] * @param {string} [q]
@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> {
return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> {
return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {SearchApiSearchRequest} requestParameters Request parameters.
@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI {
return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SearchApi
*/
public getSearchConfig(options?: AxiosRequestConfig) {
return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {SearchApiSearchRequest} requestParameters Request parameters. * @param {SearchApiSearchRequest} requestParameters Request parameters.

View File

@ -70,25 +70,26 @@
subtitle: 'Discover or synchronize sidecar metadata from the filesystem', subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
allText: 'SYNC', allText: 'SYNC',
missingText: 'DISCOVER', missingText: 'DISCOVER',
disabled: !$featureFlags.sidecar,
}, },
[JobName.ObjectTagging]: { [JobName.ObjectTagging]: {
icon: TagMultiple, icon: TagMultiple,
title: api.getJobName(JobName.ObjectTagging), title: api.getJobName(JobName.ObjectTagging),
subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected', subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected',
disabled: !$featureFlags.machineLearning, disabled: !$featureFlags.tagImage,
}, },
[JobName.ClipEncoding]: { [JobName.ClipEncoding]: {
icon: VectorCircle, icon: VectorCircle,
title: api.getJobName(JobName.ClipEncoding), title: api.getJobName(JobName.ClipEncoding),
subtitle: 'Run machine learning to generate clip embeddings', subtitle: 'Run machine learning to generate clip embeddings',
disabled: !$featureFlags.machineLearning, disabled: !$featureFlags.clipEncode,
}, },
[JobName.RecognizeFaces]: { [JobName.RecognizeFaces]: {
icon: FaceRecognition, icon: FaceRecognition,
title: api.getJobName(JobName.RecognizeFaces), title: api.getJobName(JobName.RecognizeFaces),
subtitle: 'Run machine learning to recognize faces', subtitle: 'Run machine learning to recognize faces',
handleCommand: handleFaceCommand, handleCommand: handleFaceCommand,
disabled: !$featureFlags.machineLearning, disabled: !$featureFlags.facialRecognition,
}, },
[JobName.VideoConversion]: { [JobName.VideoConversion]: {
icon: Video, icon: Video,

View File

@ -0,0 +1,104 @@
<script lang="ts">
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigDto } from '@api';
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
let config: SystemConfigDto;
let defaultConfig: SystemConfigDto;
async function refreshConfig() {
[config, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data),
api.systemConfigApi.getDefaults().then((res) => res.data),
]);
}
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
config = resetConfig;
notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info });
}
async function saveSetting() {
try {
const { data: current } = await api.systemConfigApi.getConfig();
await api.systemConfigApi.updateConfig({
systemConfigDto: { ...current, machineLearning: config.machineLearning },
});
await refreshConfig();
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
} catch (error) {
handleError(error, 'Unable to save settings');
}
}
async function resetToDefault() {
await refreshConfig();
const { data: defaults } = await api.systemConfigApi.getDefaults();
config = defaults;
notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info });
}
</script>
<div class="mt-2">
{#await refreshConfig() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault class="mx-4 flex flex-col gap-4 py-4">
<SettingSwitch
title="Enabled"
subtitle="Use machine learning features"
bind:checked={config.machineLearning.enabled}
/>
<hr />
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="URL"
desc="URL of machine learning server"
bind:value={config.machineLearning.url}
required={true}
disabled={!config.machineLearning.enabled}
isEdited={!(config.machineLearning.url === config.machineLearning.url)}
/>
<SettingSwitch
title="SMART SEARCH"
subtitle="Extract CLIP embeddings for smart search"
bind:checked={config.machineLearning.clipEncodeEnabled}
disabled={!config.machineLearning.enabled}
/>
<SettingSwitch
title="FACIAL RECOGNITION"
subtitle="Recognize and group faces in photos"
disabled={!config.machineLearning.enabled}
bind:checked={config.machineLearning.facialRecognitionEnabled}
/>
<SettingSwitch
title="IMAGE TAGGING"
subtitle="Tag and classify images"
disabled={!config.machineLearning.enabled}
bind:checked={config.machineLearning.tagImageEnabled}
/>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!isEqual(config, defaultConfig)}
/>
</form>
</div>
{/await}
</div>

View File

@ -32,9 +32,9 @@
<input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} /> <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} />
{#if disabled} {#if disabled}
<span class="slider-disable" /> <span class="slider-disable cursor-not-allowed" />
{:else} {:else}
<span class="slider" /> <span class="slider cursor-pointer" />
{/if} {/if}
</label> </label>
</div> </div>
@ -43,7 +43,6 @@
.slider, .slider,
.slider-disable { .slider-disable {
position: absolute; position: absolute;
cursor: pointer;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;

View File

@ -4,7 +4,10 @@ import { writable } from 'svelte/store';
export type FeatureFlags = ServerFeaturesDto; export type FeatureFlags = ServerFeaturesDto;
export const featureFlags = writable<FeatureFlags>({ export const featureFlags = writable<FeatureFlags>({
machineLearning: true, clipEncode: true,
facialRecognition: true,
sidecar: true,
tagImage: true,
search: true, search: true,
oauth: true, oauth: true,
oauthAutoLaunch: true, oauthAutoLaunch: true,

View File

@ -2,11 +2,12 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte';
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { api } from '@api'; import { api } from '@api';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -50,6 +51,10 @@
<OAuthSettings oauthConfig={configs.oauth} /> <OAuthSettings oauthConfig={configs.oauth} />
</SettingAccordion> </SettingAccordion>
<SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings">
<MachineLearningSettings />
</SettingAccordion>
<SettingAccordion <SettingAccordion
title="Storage Template" title="Storage Template"
subtitle="Manage the folder structure and file name of the upload asset" subtitle="Manage the folder structure and file name of the upload asset"