You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(web,server)!: configure machine learning via the UI (#3768)
This commit is contained in:
		
							
								
								
									
										141
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto { | ||||
|      */ | ||||
|     'total': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SearchConfigResponseDto | ||||
|  */ | ||||
| export interface SearchConfigResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SearchConfigResponseDto | ||||
|      */ | ||||
|     'enabled': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto { | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'machineLearning': boolean; | ||||
|     'clipEncode': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'facialRecognition': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto { | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'search': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'sidecar': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'tagImage': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -2611,6 +2616,12 @@ export interface SystemConfigDto { | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'job': SystemConfigJobDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigMachineLearningDto} | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'machineLearning': SystemConfigMachineLearningDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigOAuthDto} | ||||
| @@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto { | ||||
|      */ | ||||
|     '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 | ||||
| @@ -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); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| @@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); | ||||
|             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]  | ||||
| @@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat | ||||
|         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { | ||||
|             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. | ||||
| @@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI { | ||||
|         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. | ||||
|   | ||||
| @@ -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? | ||||
|  | ||||
| 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? | ||||
|  | ||||
| @@ -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. | ||||
| ::: | ||||
|  | ||||
| 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)? | ||||
|  | ||||
|   | ||||
| @@ -132,7 +132,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server" | ||||
|  | ||||
| IMMICH_WEB_URL=http://immich-web:3000 | ||||
| IMMICH_SERVER_URL=http://immich-server:3001 | ||||
| IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003 | ||||
|  | ||||
| #################################################################################### | ||||
| # Alternative API's External Address - Optional | ||||
|   | ||||
| @@ -50,13 +50,14 @@ These environment variables are used by the `docker-compose.yml` file and do **N | ||||
|  | ||||
| ## URLs | ||||
|  | ||||
| | Variable                      | Description                                              |                Default                | Services              | | ||||
| | :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- | | ||||
| | `IMMICH_WEB_URL`              | Immich Web URL                                           |       `http://immich-web:3000`        | 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 | | ||||
| | `PUBLIC_IMMICH_SERVER_URL`    | Public Immich URL                                        |      `http://immich-server:3001`      | web                   | | ||||
| | `IMMICH_API_URL_EXTERNAL`     | Immich API URL External                                  |                `/api`                 | web                   | | ||||
| | Variable                          | Description                  |                Default                | Services              | | ||||
| | :-------------------------------- | :--------------------------- | :-----------------------------------: | :-------------------- | | ||||
| | `IMMICH_WEB_URL`                  | Immich Web URL               |       `http://immich-web:3000`        | proxy                 | | ||||
| | `IMMICH_SERVER_URL`               | Immich Server URL            |      `http://immich-server:3001`      | web, proxy            | | ||||
| | `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                   | | ||||
| | `IMMICH_API_URL_EXTERNAL`         | Immich API URL External      |                `/api`                 | web                   | | ||||
|  | ||||
| :::info | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -84,7 +84,6 @@ doc/SearchAlbumResponseDto.md | ||||
| doc/SearchApi.md | ||||
| doc/SearchAssetDto.md | ||||
| doc/SearchAssetResponseDto.md | ||||
| doc/SearchConfigResponseDto.md | ||||
| doc/SearchExploreItem.md | ||||
| doc/SearchExploreResponseDto.md | ||||
| doc/SearchFacetCountResponseDto.md | ||||
| @@ -108,6 +107,7 @@ doc/SystemConfigApi.md | ||||
| doc/SystemConfigDto.md | ||||
| doc/SystemConfigFFmpegDto.md | ||||
| doc/SystemConfigJobDto.md | ||||
| doc/SystemConfigMachineLearningDto.md | ||||
| doc/SystemConfigOAuthDto.md | ||||
| doc/SystemConfigPasswordLoginDto.md | ||||
| doc/SystemConfigStorageTemplateDto.md | ||||
| @@ -228,7 +228,6 @@ lib/model/queue_status_dto.dart | ||||
| lib/model/search_album_response_dto.dart | ||||
| lib/model/search_asset_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_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_f_fmpeg_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_password_login_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_asset_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_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_f_fmpeg_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_password_login_dto_test.dart | ||||
| test/system_config_storage_template_dto_test.dart | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -140,7 +140,6 @@ Class | Method | HTTP request | Description | ||||
| *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |  | ||||
| *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |  | ||||
| *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 |  | ||||
| *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |  | ||||
| *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |  | ||||
| @@ -253,7 +252,6 @@ Class | Method | HTTP request | Description | ||||
|  - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) | ||||
|  - [SearchAssetDto](doc//SearchAssetDto.md) | ||||
|  - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) | ||||
|  - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md) | ||||
|  - [SearchExploreItem](doc//SearchExploreItem.md) | ||||
|  - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md) | ||||
|  - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) | ||||
| @@ -274,6 +272,7 @@ Class | Method | HTTP request | Description | ||||
|  - [SystemConfigDto](doc//SystemConfigDto.md) | ||||
|  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) | ||||
|  - [SystemConfigJobDto](doc//SystemConfigJobDto.md) | ||||
|  - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md) | ||||
|  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) | ||||
|  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) | ||||
|  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) | ||||
|   | ||||
							
								
								
									
										52
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							| @@ -10,7 +10,6 @@ All URIs are relative to */api* | ||||
| Method | HTTP request | Description | ||||
| ------------- | ------------- | ------------- | ||||
| [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |  | ||||
| [**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config |  | ||||
| [**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) | ||||
| 
 | ||||
| # **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** | ||||
| > SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							| @@ -8,11 +8,14 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **machineLearning** | **bool** |  |  | ||||
| **clipEncode** | **bool** |  |  | ||||
| **facialRecognition** | **bool** |  |  | ||||
| **oauth** | **bool** |  |  | ||||
| **oauthAutoLaunch** | **bool** |  |  | ||||
| **passwordLogin** | **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) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							| @@ -10,6 +10,7 @@ Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  |  | ||||
| **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  |  | ||||
| **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) |  |  | ||||
| **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  |  | ||||
| **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  |  | ||||
| **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  |  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # openapi.model.SearchConfigResponseDto | ||||
| # openapi.model.SystemConfigMachineLearningDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| @@ -8,7 +8,11 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **clipEncodeEnabled** | **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) | ||||
| 
 | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -115,7 +115,6 @@ part 'model/queue_status_dto.dart'; | ||||
| part 'model/search_album_response_dto.dart'; | ||||
| part 'model/search_asset_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_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_f_fmpeg_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_password_login_dto.dart'; | ||||
| part 'model/system_config_storage_template_dto.dart'; | ||||
|   | ||||
							
								
								
									
										41
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										41
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -60,47 +60,6 @@ class SearchApi { | ||||
|     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]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -323,8 +323,6 @@ class ApiClient { | ||||
|           return SearchAssetDto.fromJson(value); | ||||
|         case 'SearchAssetResponseDto': | ||||
|           return SearchAssetResponseDto.fromJson(value); | ||||
|         case 'SearchConfigResponseDto': | ||||
|           return SearchConfigResponseDto.fromJson(value); | ||||
|         case 'SearchExploreItem': | ||||
|           return SearchExploreItem.fromJson(value); | ||||
|         case 'SearchExploreResponseDto': | ||||
| @@ -365,6 +363,8 @@ class ApiClient { | ||||
|           return SystemConfigFFmpegDto.fromJson(value); | ||||
|         case 'SystemConfigJobDto': | ||||
|           return SystemConfigJobDto.fromJson(value); | ||||
|         case 'SystemConfigMachineLearningDto': | ||||
|           return SystemConfigMachineLearningDto.fromJson(value); | ||||
|         case 'SystemConfigOAuthDto': | ||||
|           return SystemConfigOAuthDto.fromJson(value); | ||||
|         case 'SystemConfigPasswordLoginDto': | ||||
|   | ||||
| @@ -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', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										44
									
								
								mobile/openapi/lib/model/server_features_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								mobile/openapi/lib/model/server_features_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -13,14 +13,19 @@ part of openapi.api; | ||||
| class ServerFeaturesDto { | ||||
|   /// Returns a new [ServerFeaturesDto] instance. | ||||
|   ServerFeaturesDto({ | ||||
|     required this.machineLearning, | ||||
|     required this.clipEncode, | ||||
|     required this.facialRecognition, | ||||
|     required this.oauth, | ||||
|     required this.oauthAutoLaunch, | ||||
|     required this.passwordLogin, | ||||
|     required this.search, | ||||
|     required this.sidecar, | ||||
|     required this.tagImage, | ||||
|   }); | ||||
| 
 | ||||
|   bool machineLearning; | ||||
|   bool clipEncode; | ||||
| 
 | ||||
|   bool facialRecognition; | ||||
| 
 | ||||
|   bool oauth; | ||||
| 
 | ||||
| @@ -30,33 +35,46 @@ class ServerFeaturesDto { | ||||
| 
 | ||||
|   bool search; | ||||
| 
 | ||||
|   bool sidecar; | ||||
| 
 | ||||
|   bool tagImage; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto && | ||||
|      other.machineLearning == machineLearning && | ||||
|      other.clipEncode == clipEncode && | ||||
|      other.facialRecognition == facialRecognition && | ||||
|      other.oauth == oauth && | ||||
|      other.oauthAutoLaunch == oauthAutoLaunch && | ||||
|      other.passwordLogin == passwordLogin && | ||||
|      other.search == search; | ||||
|      other.search == search && | ||||
|      other.sidecar == sidecar && | ||||
|      other.tagImage == tagImage; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (machineLearning.hashCode) + | ||||
|     (clipEncode.hashCode) + | ||||
|     (facialRecognition.hashCode) + | ||||
|     (oauth.hashCode) + | ||||
|     (oauthAutoLaunch.hashCode) + | ||||
|     (passwordLogin.hashCode) + | ||||
|     (search.hashCode); | ||||
|     (search.hashCode) + | ||||
|     (sidecar.hashCode) + | ||||
|     (tagImage.hashCode); | ||||
| 
 | ||||
|   @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() { | ||||
|     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'oauthAutoLaunch'] = this.oauthAutoLaunch; | ||||
|       json[r'passwordLogin'] = this.passwordLogin; | ||||
|       json[r'search'] = this.search; | ||||
|       json[r'sidecar'] = this.sidecar; | ||||
|       json[r'tagImage'] = this.tagImage; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -68,11 +86,14 @@ class ServerFeaturesDto { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       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')!, | ||||
|         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!, | ||||
|         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!, | ||||
|         search: mapValueOfType<bool>(json, r'search')!, | ||||
|         sidecar: mapValueOfType<bool>(json, r'sidecar')!, | ||||
|         tagImage: mapValueOfType<bool>(json, r'tagImage')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
| @@ -120,11 +141,14 @@ class ServerFeaturesDto { | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'machineLearning', | ||||
|     'clipEncode', | ||||
|     'facialRecognition', | ||||
|     'oauth', | ||||
|     'oauthAutoLaunch', | ||||
|     'passwordLogin', | ||||
|     'search', | ||||
|     'sidecar', | ||||
|     'tagImage', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -15,6 +15,7 @@ class SystemConfigDto { | ||||
|   SystemConfigDto({ | ||||
|     required this.ffmpeg, | ||||
|     required this.job, | ||||
|     required this.machineLearning, | ||||
|     required this.oauth, | ||||
|     required this.passwordLogin, | ||||
|     required this.storageTemplate, | ||||
| @@ -25,6 +26,8 @@ class SystemConfigDto { | ||||
| 
 | ||||
|   SystemConfigJobDto job; | ||||
| 
 | ||||
|   SystemConfigMachineLearningDto machineLearning; | ||||
| 
 | ||||
|   SystemConfigOAuthDto oauth; | ||||
| 
 | ||||
|   SystemConfigPasswordLoginDto passwordLogin; | ||||
| @@ -37,6 +40,7 @@ class SystemConfigDto { | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && | ||||
|      other.ffmpeg == ffmpeg && | ||||
|      other.job == job && | ||||
|      other.machineLearning == machineLearning && | ||||
|      other.oauth == oauth && | ||||
|      other.passwordLogin == passwordLogin && | ||||
|      other.storageTemplate == storageTemplate && | ||||
| @@ -47,18 +51,20 @@ class SystemConfigDto { | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (ffmpeg.hashCode) + | ||||
|     (job.hashCode) + | ||||
|     (machineLearning.hashCode) + | ||||
|     (oauth.hashCode) + | ||||
|     (passwordLogin.hashCode) + | ||||
|     (storageTemplate.hashCode) + | ||||
|     (thumbnail.hashCode); | ||||
| 
 | ||||
|   @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() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'ffmpeg'] = this.ffmpeg; | ||||
|       json[r'job'] = this.job; | ||||
|       json[r'machineLearning'] = this.machineLearning; | ||||
|       json[r'oauth'] = this.oauth; | ||||
|       json[r'passwordLogin'] = this.passwordLogin; | ||||
|       json[r'storageTemplate'] = this.storageTemplate; | ||||
| @@ -76,6 +82,7 @@ class SystemConfigDto { | ||||
|       return SystemConfigDto( | ||||
|         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, | ||||
|         job: SystemConfigJobDto.fromJson(json[r'job'])!, | ||||
|         machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!, | ||||
|         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, | ||||
|         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, | ||||
|         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, | ||||
| @@ -129,6 +136,7 @@ class SystemConfigDto { | ||||
|   static const requiredKeys = <String>{ | ||||
|     'ffmpeg', | ||||
|     'job', | ||||
|     'machineLearning', | ||||
|     'oauth', | ||||
|     'passwordLogin', | ||||
|     'storageTemplate', | ||||
|   | ||||
							
								
								
									
										130
									
								
								mobile/openapi/lib/model/system_config_machine_learning_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								mobile/openapi/lib/model/system_config_machine_learning_dto.dart
									
									
									
										generated
									
									
									
										Normal 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', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -22,11 +22,6 @@ void main() { | ||||
|       // 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 | ||||
|     test('test search', () async { | ||||
|       // TODO | ||||
|   | ||||
| @@ -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 | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										19
									
								
								mobile/openapi/test/server_features_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/test/server_features_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,8 +16,13 @@ void main() { | ||||
|   // final instance = ServerFeaturesDto(); | ||||
| 
 | ||||
|   group('test ServerFeaturesDto', () { | ||||
|     // bool machineLearning | ||||
|     test('to test the property `machineLearning`', () async { | ||||
|     // bool clipEncode | ||||
|     test('to test the property `clipEncode`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool facialRecognition | ||||
|     test('to test the property `facialRecognition`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| @@ -41,6 +46,16 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool sidecar | ||||
|     test('to test the property `sidecar`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool tagImage | ||||
|     test('to test the property `tagImage`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -26,6 +26,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // SystemConfigMachineLearningDto machineLearning | ||||
|     test('to test the property `machineLearning`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // SystemConfigOAuthDto oauth | ||||
|     test('to test the property `oauth`', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										47
									
								
								mobile/openapi/test/system_config_machine_learning_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mobile/openapi/test/system_config_machine_learning_dto_test.dart
									
									
									
										generated
									
									
									
										Normal 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 | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -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": { | ||||
|       "get": { | ||||
|         "operationId": "getExploreData", | ||||
| @@ -6424,17 +6392,6 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SearchConfigResponseDto": { | ||||
|         "properties": { | ||||
|           "enabled": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "enabled" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SearchExploreItem": { | ||||
|         "properties": { | ||||
|           "data": { | ||||
| @@ -6518,7 +6475,10 @@ | ||||
|       }, | ||||
|       "ServerFeaturesDto": { | ||||
|         "properties": { | ||||
|           "machineLearning": { | ||||
|           "clipEncode": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "facialRecognition": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "oauth": { | ||||
| @@ -6532,11 +6492,20 @@ | ||||
|           }, | ||||
|           "search": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "sidecar": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "tagImage": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "machineLearning", | ||||
|           "clipEncode", | ||||
|           "facialRecognition", | ||||
|           "sidecar", | ||||
|           "search", | ||||
|           "tagImage", | ||||
|           "oauth", | ||||
|           "oauthAutoLaunch", | ||||
|           "passwordLogin" | ||||
| @@ -6868,6 +6837,9 @@ | ||||
|           "job": { | ||||
|             "$ref": "#/components/schemas/SystemConfigJobDto" | ||||
|           }, | ||||
|           "machineLearning": { | ||||
|             "$ref": "#/components/schemas/SystemConfigMachineLearningDto" | ||||
|           }, | ||||
|           "oauth": { | ||||
|             "$ref": "#/components/schemas/SystemConfigOAuthDto" | ||||
|           }, | ||||
| @@ -6883,6 +6855,7 @@ | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ffmpeg", | ||||
|           "machineLearning", | ||||
|           "oauth", | ||||
|           "passwordLogin", | ||||
|           "storageTemplate", | ||||
| @@ -6989,6 +6962,33 @@ | ||||
|         ], | ||||
|         "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": { | ||||
|         "properties": { | ||||
|           "autoLaunch": { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { AssetType } from '@app/infra/entities'; | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { Duration } from 'luxon'; | ||||
| import { extname } from 'node:path'; | ||||
| 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 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[]> = { | ||||
|   '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], | ||||
|   '.ari': ['image/ari', 'image/x-arriflex-ari'], | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { | ||||
|   newPersonRepositoryMock, | ||||
|   newSearchRepositoryMock, | ||||
|   newStorageRepositoryMock, | ||||
|   newSystemConfigRepositoryMock, | ||||
|   personStub, | ||||
| } from '@test'; | ||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | ||||
| @@ -18,6 +19,7 @@ import { IPersonRepository } from '../person'; | ||||
| import { ISearchRepository } from '../search'; | ||||
| import { IMachineLearningRepository } from '../smart-info'; | ||||
| import { IStorageRepository } from '../storage'; | ||||
| import { ISystemConfigRepository } from '../system-config'; | ||||
| import { IFaceRepository } from './face.repository'; | ||||
| import { FacialRecognitionService } from './facial-recognition.services'; | ||||
|  | ||||
| @@ -94,6 +96,7 @@ const faceSearch = { | ||||
| describe(FacialRecognitionService.name, () => { | ||||
|   let sut: FacialRecognitionService; | ||||
|   let assetMock: jest.Mocked<IAssetRepository>; | ||||
|   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||
|   let faceMock: jest.Mocked<IFaceRepository>; | ||||
|   let jobMock: jest.Mocked<IJobRepository>; | ||||
|   let machineLearningMock: jest.Mocked<IMachineLearningRepository>; | ||||
| @@ -104,6 +107,7 @@ describe(FacialRecognitionService.name, () => { | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     assetMock = newAssetRepositoryMock(); | ||||
|     configMock = newSystemConfigRepositoryMock(); | ||||
|     faceMock = newFaceRepositoryMock(); | ||||
|     jobMock = newJobRepositoryMock(); | ||||
|     machineLearningMock = newMachineLearningRepositoryMock(); | ||||
| @@ -116,6 +120,7 @@ describe(FacialRecognitionService.name, () => { | ||||
|  | ||||
|     sut = new FacialRecognitionService( | ||||
|       assetMock, | ||||
|       configMock, | ||||
|       faceMock, | ||||
|       jobMock, | ||||
|       machineLearningMock, | ||||
| @@ -174,7 +179,7 @@ describe(FacialRecognitionService.name, () => { | ||||
|       machineLearningMock.detectFaces.mockResolvedValue([]); | ||||
|       assetMock.getByIds.mockResolvedValue([assetStub.image]); | ||||
|       await sut.handleRecognizeFaces({ id: assetStub.image.id }); | ||||
|       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith({ | ||||
|       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith('http://immich-machine-learning:3003', { | ||||
|         imagePath: assetStub.image.resizePath, | ||||
|       }); | ||||
|       expect(faceMock.create).not.toHaveBeenCalled(); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { Inject, Logger } from '@nestjs/common'; | ||||
| import { join } from 'path'; | ||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | ||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; | ||||
| import { usePagination } from '../domain.util'; | ||||
| import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||
| 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 { IMachineLearningRepository } from '../smart-info'; | ||||
| import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||
| import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||
| import { AssetFaceId, IFaceRepository } from './face.repository'; | ||||
|  | ||||
| export class FacialRecognitionService { | ||||
|   private logger = new Logger(FacialRecognitionService.name); | ||||
|   private storageCore = new StorageCore(); | ||||
|   private configCore: SystemConfigCore; | ||||
|  | ||||
|   constructor( | ||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||
|     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||
|     @Inject(IFaceRepository) private faceRepository: IFaceRepository, | ||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||
| @@ -24,9 +26,16 @@ export class FacialRecognitionService { | ||||
|     @Inject(IPersonRepository) private personRepository: IPersonRepository, | ||||
|     @Inject(ISearchRepository) private searchRepository: ISearchRepository, | ||||
|     @Inject(IStorageRepository) private storageRepository: IStorageRepository, | ||||
|   ) {} | ||||
|   ) { | ||||
|     this.configCore = new SystemConfigCore(configRepository); | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       return force | ||||
|         ? this.assetRepository.getAll(pagination, { order: 'DESC' }) | ||||
| @@ -49,12 +58,17 @@ export class FacialRecognitionService { | ||||
|   } | ||||
|  | ||||
|   async handleRecognizeFaces({ id }: IEntityJob) { | ||||
|     const { machineLearning } = await this.configCore.getConfig(); | ||||
|     if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) { | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     const [asset] = await this.assetRepository.getByIds([id]); | ||||
|     if (!asset || !MACHINE_LEARNING_ENABLED || !asset.resizePath) { | ||||
|     if (!asset || !asset.resizePath) { | ||||
|       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.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` }))); | ||||
| @@ -100,6 +114,11 @@ export class FacialRecognitionService { | ||||
|   } | ||||
|  | ||||
|   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 [asset] = await this.assetRepository.getByIds([assetId]); | ||||
|   | ||||
| @@ -2,8 +2,7 @@ import { AssetType } from '@app/infra/entities'; | ||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { IAssetRepository, mapAsset } from '../asset'; | ||||
| import { CommunicationEvent, ICommunicationRepository } from '../communication'; | ||||
| import { assertMachineLearningEnabled } from '../domain.constant'; | ||||
| import { ISystemConfigRepository } from '../system-config'; | ||||
| import { FeatureFlag, ISystemConfigRepository } from '../system-config'; | ||||
| import { SystemConfigCore } from '../system-config/system-config.core'; | ||||
| import { JobCommand, JobName, QueueName } from './job.constants'; | ||||
| import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto'; | ||||
| @@ -78,23 +77,25 @@ export class JobService { | ||||
|         return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); | ||||
|  | ||||
|       case QueueName.OBJECT_TAGGING: | ||||
|         assertMachineLearningEnabled(); | ||||
|         await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE); | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } }); | ||||
|  | ||||
|       case QueueName.CLIP_ENCODING: | ||||
|         assertMachineLearningEnabled(); | ||||
|         await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE); | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); | ||||
|  | ||||
|       case QueueName.METADATA_EXTRACTION: | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); | ||||
|  | ||||
|       case QueueName.SIDECAR: | ||||
|         await this.configCore.requireFeature(FeatureFlag.SIDECAR); | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); | ||||
|  | ||||
|       case QueueName.THUMBNAIL_GENERATION: | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); | ||||
|  | ||||
|       case QueueName.RECOGNIZE_FACES: | ||||
|         await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION); | ||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } }); | ||||
|  | ||||
|       default: | ||||
|   | ||||
| @@ -1,3 +1,2 @@ | ||||
| export * from './search-config-response.dto'; | ||||
| export * from './search-explore.response.dto'; | ||||
| export * from './search-response.dto'; | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| export class SearchConfigResponseDto { | ||||
|   enabled!: boolean; | ||||
| } | ||||
| @@ -1,5 +1,3 @@ | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { ConfigService } from '@nestjs/config'; | ||||
| import { | ||||
|   albumStub, | ||||
|   assetStub, | ||||
| @@ -12,12 +10,14 @@ import { | ||||
|   newJobRepositoryMock, | ||||
|   newMachineLearningRepositoryMock, | ||||
|   newSearchRepositoryMock, | ||||
|   newSystemConfigRepositoryMock, | ||||
|   searchStub, | ||||
| } from '@test'; | ||||
| import { plainToInstance } from 'class-transformer'; | ||||
| import { IAlbumRepository } from '../album/album.repository'; | ||||
| import { IAssetRepository } from '../asset/asset.repository'; | ||||
| import { IFaceRepository } from '../facial-recognition'; | ||||
| import { ISystemConfigRepository } from '../index'; | ||||
| import { JobName } from '../job'; | ||||
| import { IJobRepository } from '../job/job.repository'; | ||||
| import { IMachineLearningRepository } from '../smart-info'; | ||||
| @@ -31,29 +31,26 @@ describe(SearchService.name, () => { | ||||
|   let sut: SearchService; | ||||
|   let albumMock: jest.Mocked<IAlbumRepository>; | ||||
|   let assetMock: jest.Mocked<IAssetRepository>; | ||||
|   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||
|   let faceMock: jest.Mocked<IFaceRepository>; | ||||
|   let jobMock: jest.Mocked<IJobRepository>; | ||||
|   let machineMock: jest.Mocked<IMachineLearningRepository>; | ||||
|   let searchMock: jest.Mocked<ISearchRepository>; | ||||
|   let configMock: jest.Mocked<ConfigService>; | ||||
|  | ||||
|   const makeSut = (value?: string) => { | ||||
|     if (value) { | ||||
|       configMock.get.mockReturnValue(value); | ||||
|     } | ||||
|     return new SearchService(albumMock, assetMock, faceMock, jobMock, machineMock, searchMock, configMock); | ||||
|   }; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|   beforeEach(async () => { | ||||
|     albumMock = newAlbumRepositoryMock(); | ||||
|     assetMock = newAssetRepositoryMock(); | ||||
|     configMock = newSystemConfigRepositoryMock(); | ||||
|     faceMock = newFaceRepositoryMock(); | ||||
|     jobMock = newJobRepositoryMock(); | ||||
|     machineMock = newMachineLearningRepositoryMock(); | ||||
|     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(() => { | ||||
| @@ -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`, () => { | ||||
|     it('should skip when search is disabled', async () => { | ||||
|       const sut = makeSut('false'); | ||||
|     // it('should skip when search is disabled', async () => { | ||||
|     //   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(); | ||||
|       expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled(); | ||||
|       expect(jobMock.queue).not.toHaveBeenCalled(); | ||||
|  | ||||
|       sut.teardown(); | ||||
|     }); | ||||
|     //   sut.teardown(); | ||||
|     // }); | ||||
|  | ||||
|     it('should skip schema migration if not needed', async () => { | ||||
|       searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false }); | ||||
|       await sut.init(); | ||||
|  | ||||
|       expect(searchMock.setup).toHaveBeenCalled(); | ||||
| @@ -145,14 +115,14 @@ describe(SearchService.name, () => { | ||||
|   }); | ||||
|  | ||||
|   describe('search', () => { | ||||
|     it('should throw an error is search is disabled', async () => { | ||||
|       const sut = makeSut('false'); | ||||
|     // it('should throw an error is search is disabled', async () => { | ||||
|     //   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.searchAssets).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|     //   expect(searchMock.searchAlbums).not.toHaveBeenCalled(); | ||||
|     //   expect(searchMock.searchAssets).not.toHaveBeenCalled(); | ||||
|     // }); | ||||
|  | ||||
|     it('should search assets and albums', async () => { | ||||
|       searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults); | ||||
| @@ -205,7 +175,7 @@ describe(SearchService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should skip if search is disabled', async () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|  | ||||
|       await sut.handleIndexAssets(); | ||||
|  | ||||
| @@ -216,7 +186,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleIndexAsset', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleIndexAsset({ ids: [assetStub.image.id] }); | ||||
|     }); | ||||
|  | ||||
| @@ -227,7 +197,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleIndexAlbums', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleIndexAlbums(); | ||||
|     }); | ||||
|  | ||||
| @@ -242,7 +212,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleIndexAlbum', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleIndexAlbum({ ids: [albumStub.empty.id] }); | ||||
|     }); | ||||
|  | ||||
| @@ -253,7 +223,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleRemoveAlbum', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleRemoveAlbum({ ids: ['album1'] }); | ||||
|     }); | ||||
|  | ||||
| @@ -264,7 +234,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleRemoveAsset', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleRemoveAsset({ ids: ['asset1'] }); | ||||
|     }); | ||||
|  | ||||
| @@ -305,7 +275,7 @@ describe(SearchService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should skip if search is disabled', async () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|  | ||||
|       await sut.handleIndexFaces(); | ||||
|  | ||||
| @@ -315,7 +285,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleIndexAsset', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' }); | ||||
|  | ||||
|       expect(searchMock.importFaces).not.toHaveBeenCalled(); | ||||
| @@ -333,7 +303,7 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|   describe('handleRemoveFace', () => { | ||||
|     it('should skip if search is disabled', () => { | ||||
|       const sut = makeSut('false'); | ||||
|       sut['enabled'] = false; | ||||
|       sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' }); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,17 @@ | ||||
| import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities'; | ||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { ConfigService } from '@nestjs/config'; | ||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { mapAlbumWithAssets } from '../album'; | ||||
| import { IAlbumRepository } from '../album/album.repository'; | ||||
| import { AssetResponseDto, mapAsset } from '../asset'; | ||||
| import { IAssetRepository } from '../asset/asset.repository'; | ||||
| import { AuthUserDto } from '../auth'; | ||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; | ||||
| import { usePagination } from '../domain.util'; | ||||
| import { AssetFaceId, IFaceRepository } from '../facial-recognition'; | ||||
| import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||
| import { IMachineLearningRepository } from '../smart-info'; | ||||
| import { FeatureFlag, ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||
| import { SearchDto } from './dto'; | ||||
| import { SearchConfigResponseDto, SearchResponseDto } from './response-dto'; | ||||
| import { SearchResponseDto } from './response-dto'; | ||||
| import { | ||||
|   ISearchRepository, | ||||
|   OwnedFaceEntity, | ||||
| @@ -30,8 +29,9 @@ interface SyncQueue { | ||||
| @Injectable() | ||||
| export class SearchService { | ||||
|   private logger = new Logger(SearchService.name); | ||||
|   private enabled: boolean; | ||||
|   private enabled = false; | ||||
|   private timer: NodeJS.Timer | null = null; | ||||
|   private configCore: SystemConfigCore; | ||||
|  | ||||
|   private albumQueue: SyncQueue = { | ||||
|     upsert: new Set(), | ||||
| @@ -51,16 +51,13 @@ export class SearchService { | ||||
|   constructor( | ||||
|     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, | ||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||
|     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||
|     @Inject(IFaceRepository) private faceRepository: IFaceRepository, | ||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||
|     @Inject(ISearchRepository) private searchRepository: ISearchRepository, | ||||
|     configService: ConfigService, | ||||
|   ) { | ||||
|     this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false'; | ||||
|     if (this.enabled) { | ||||
|       this.timer = setInterval(() => this.flush(), 5_000); | ||||
|     } | ||||
|     this.configCore = new SystemConfigCore(configRepository); | ||||
|   } | ||||
|  | ||||
|   teardown() { | ||||
| @@ -70,17 +67,8 @@ export class SearchService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   isEnabled() { | ||||
|     return this.enabled; | ||||
|   } | ||||
|  | ||||
|   getConfig(): SearchConfigResponseDto { | ||||
|     return { | ||||
|       enabled: this.enabled, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   async init() { | ||||
|     this.enabled = await this.configCore.hasFeature(FeatureFlag.SEARCH); | ||||
|     if (!this.enabled) { | ||||
|       return; | ||||
|     } | ||||
| @@ -101,10 +89,13 @@ export class SearchService { | ||||
|       this.logger.debug('Queueing job to re-index all faces'); | ||||
|       await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES }); | ||||
|     } | ||||
|  | ||||
|     this.timer = setInterval(() => this.flush(), 5_000); | ||||
|   } | ||||
|  | ||||
|   async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> { | ||||
|     this.assertEnabled(); | ||||
|     await this.configCore.requireFeature(FeatureFlag.SEARCH); | ||||
|  | ||||
|     const results = await this.searchRepository.explore(authUser.id); | ||||
|     const lookup = await this.getLookupMap( | ||||
|       results.reduce( | ||||
| @@ -126,16 +117,18 @@ export class SearchService { | ||||
|   } | ||||
|  | ||||
|   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 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 }; | ||||
|  | ||||
|     let assets: SearchResult<AssetEntity>; | ||||
|     switch (strategy) { | ||||
|       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); | ||||
|         break; | ||||
|       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[]> { | ||||
|     const entities = await this.albumRepository.getByIds(ids); | ||||
|     return this.patchAlbums(entities); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { IServerVersion } from '@app/domain'; | ||||
| import { FeatureFlags, IServerVersion } from '@app/domain'; | ||||
| import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; | ||||
|  | ||||
| export class ServerPingResponse { | ||||
| @@ -79,10 +79,14 @@ export class ServerMediaTypesResponseDto { | ||||
|   sidecar!: string[]; | ||||
| } | ||||
|  | ||||
| export class ServerFeaturesDto { | ||||
|   machineLearning!: boolean; | ||||
| export class ServerFeaturesDto implements FeatureFlags { | ||||
|   clipEncode!: boolean; | ||||
|   facialRecognition!: boolean; | ||||
|   sidecar!: boolean; | ||||
|   search!: boolean; | ||||
|   tagImage!: boolean; | ||||
|  | ||||
|   // TODO: use these instead of `POST oauth/config` | ||||
|   oauth!: boolean; | ||||
|   oauthAutoLaunch!: boolean; | ||||
|   passwordLogin!: boolean; | ||||
|   | ||||
| @@ -147,11 +147,14 @@ describe(ServerInfoService.name, () => { | ||||
|     describe('getFeatures', () => { | ||||
|       it('should respond the server features', async () => { | ||||
|         await expect(sut.getFeatures()).resolves.toEqual({ | ||||
|           machineLearning: true, | ||||
|           clipEncode: true, | ||||
|           facialRecognition: true, | ||||
|           oauth: false, | ||||
|           oauthAutoLaunch: false, | ||||
|           passwordLogin: true, | ||||
|           search: true, | ||||
|           sidecar: true, | ||||
|           tagImage: true, | ||||
|         }); | ||||
|         expect(configMock.load).toHaveBeenCalled(); | ||||
|       }); | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| 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 { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||
| import { ISystemConfigRepository } from '../system-config'; | ||||
| import { SystemConfigCore } from '../system-config/system-config.core'; | ||||
| import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||
| import { IUserRepository, UserStatsQueryResponse } from '../user'; | ||||
| import { | ||||
|   ServerFeaturesDto, | ||||
| @@ -52,18 +51,8 @@ export class ServerInfoService { | ||||
|     return serverVersion; | ||||
|   } | ||||
|  | ||||
|   async getFeatures(): Promise<ServerFeaturesDto> { | ||||
|     const config = await this.configCore.getConfig(); | ||||
|  | ||||
|     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, | ||||
|     }; | ||||
|   getFeatures(): Promise<ServerFeaturesDto> { | ||||
|     return this.configCore.getFeatures(); | ||||
|   } | ||||
|  | ||||
|   async getStats(): Promise<ServerStatsResponseDto> { | ||||
|   | ||||
| @@ -20,8 +20,8 @@ export interface DetectFaceResult { | ||||
| } | ||||
|  | ||||
| export interface IMachineLearningRepository { | ||||
|   classifyImage(input: MachineLearningInput): Promise<string[]>; | ||||
|   encodeImage(input: MachineLearningInput): Promise<number[]>; | ||||
|   encodeText(input: string): Promise<number[]>; | ||||
|   detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]>; | ||||
|   classifyImage(url: string, input: MachineLearningInput): Promise<string[]>; | ||||
|   encodeImage(url: string, input: MachineLearningInput): Promise<number[]>; | ||||
|   encodeText(url: string, input: string): Promise<number[]>; | ||||
|   detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]>; | ||||
| } | ||||
|   | ||||
| @@ -5,9 +5,11 @@ import { | ||||
|   newJobRepositoryMock, | ||||
|   newMachineLearningRepositoryMock, | ||||
|   newSmartInfoRepositoryMock, | ||||
|   newSystemConfigRepositoryMock, | ||||
| } from '@test'; | ||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { ISystemConfigRepository } from '../system-config'; | ||||
| import { IMachineLearningRepository } from './machine-learning.interface'; | ||||
| import { ISmartInfoRepository } from './smart-info.repository'; | ||||
| import { SmartInfoService } from './smart-info.service'; | ||||
| @@ -20,16 +22,18 @@ const asset = { | ||||
| describe(SmartInfoService.name, () => { | ||||
|   let sut: SmartInfoService; | ||||
|   let assetMock: jest.Mocked<IAssetRepository>; | ||||
|   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||
|   let jobMock: jest.Mocked<IJobRepository>; | ||||
|   let smartMock: jest.Mocked<ISmartInfoRepository>; | ||||
|   let machineMock: jest.Mocked<IMachineLearningRepository>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     assetMock = newAssetRepositoryMock(); | ||||
|     configMock = newSystemConfigRepositoryMock(); | ||||
|     smartMock = newSmartInfoRepositoryMock(); | ||||
|     jobMock = newJobRepositoryMock(); | ||||
|     machineMock = newMachineLearningRepositoryMock(); | ||||
|     sut = new SmartInfoService(assetMock, jobMock, smartMock, machineMock); | ||||
|     sut = new SmartInfoService(assetMock, configMock, jobMock, smartMock, machineMock); | ||||
|  | ||||
|     assetMock.getByIds.mockResolvedValue([asset]); | ||||
|   }); | ||||
| @@ -80,7 +84,9 @@ describe(SmartInfoService.name, () => { | ||||
|  | ||||
|       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({ | ||||
|         assetId: 'asset-1', | ||||
|         tags: ['tag1', 'tag2', 'tag3'], | ||||
| @@ -139,7 +145,9 @@ describe(SmartInfoService.name, () => { | ||||
|  | ||||
|       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({ | ||||
|         assetId: 'asset-1', | ||||
|         clipEmbedding: [0.01, 0.02, 0.03], | ||||
|   | ||||
| @@ -1,23 +1,31 @@ | ||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | ||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; | ||||
| import { usePagination } from '../domain.util'; | ||||
| import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||
| import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||
| import { IMachineLearningRepository } from './machine-learning.interface'; | ||||
| import { ISmartInfoRepository } from './smart-info.repository'; | ||||
|  | ||||
| @Injectable() | ||||
| export class SmartInfoService { | ||||
|   private logger = new Logger(SmartInfoService.name); | ||||
|   private configCore: SystemConfigCore; | ||||
|  | ||||
|   constructor( | ||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||
|     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||
|     @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, | ||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||
|   ) {} | ||||
|   ) { | ||||
|     this.configCore = new SystemConfigCore(configRepository); | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       return force | ||||
|         ? this.assetRepository.getAll(pagination) | ||||
| @@ -34,19 +42,28 @@ export class SmartInfoService { | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|     } | ||||
|  | ||||
|     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 }); | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       return force | ||||
|         ? this.assetRepository.getAll(pagination) | ||||
| @@ -63,13 +80,17 @@ export class SmartInfoService { | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|     } | ||||
|  | ||||
|     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 }); | ||||
|  | ||||
|     return true; | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -4,16 +4,22 @@ import { Type } from 'class-transformer'; | ||||
| import { IsObject, ValidateNested } from 'class-validator'; | ||||
| import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; | ||||
| import { SystemConfigJobDto } from './system-config-job.dto'; | ||||
| import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto'; | ||||
| import { SystemConfigOAuthDto } from './system-config-oauth.dto'; | ||||
| import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; | ||||
| import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; | ||||
|  | ||||
| export class SystemConfigDto { | ||||
| export class SystemConfigDto implements SystemConfig { | ||||
|   @Type(() => SystemConfigFFmpegDto) | ||||
|   @ValidateNested() | ||||
|   @IsObject() | ||||
|   ffmpeg!: SystemConfigFFmpegDto; | ||||
|  | ||||
|   @Type(() => SystemConfigMachineLearningDto) | ||||
|   @ValidateNested() | ||||
|   @IsObject() | ||||
|   machineLearning!: SystemConfigMachineLearningDto; | ||||
|  | ||||
|   @Type(() => SystemConfigOAuthDto) | ||||
|   @ValidateNested() | ||||
|   @IsObject() | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| export * from './dto'; | ||||
| export * from './response-dto'; | ||||
| export * from './system-config.constants'; | ||||
| export * from './system-config.core'; | ||||
| export * from './system-config.repository'; | ||||
| export * from './system-config.service'; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { | ||||
|   TranscodePolicy, | ||||
|   VideoCodec, | ||||
| } from '@app/infra/entities'; | ||||
| import { BadRequestException, Injectable, Logger } from '@nestjs/common'; | ||||
| import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common'; | ||||
| import * as _ from 'lodash'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { DeepPartial } from 'typeorm'; | ||||
| @@ -44,6 +44,13 @@ export const defaults = Object.freeze<SystemConfig>({ | ||||
|     [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, | ||||
|     [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: { | ||||
|     enabled: false, | ||||
|     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>(); | ||||
|  | ||||
| @Injectable() | ||||
| @@ -82,6 +102,53 @@ export class SystemConfigCore { | ||||
|  | ||||
|   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 { | ||||
|     return defaults; | ||||
|   } | ||||
|   | ||||
| @@ -46,6 +46,13 @@ const updatedConfig = Object.freeze<SystemConfig>({ | ||||
|     accel: TranscodeHWAccel.DISABLED, | ||||
|     tonemap: ToneMapping.HABLE, | ||||
|   }, | ||||
|   machineLearning: { | ||||
|     enabled: true, | ||||
|     url: 'http://immich-machine-learning:3003', | ||||
|     facialRecognitionEnabled: true, | ||||
|     tagImageEnabled: true, | ||||
|     clipEncodeEnabled: true, | ||||
|   }, | ||||
|   oauth: { | ||||
|     autoLaunch: true, | ||||
|     autoRegister: true, | ||||
|   | ||||
| @@ -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 { Cron, CronExpression } from '@nestjs/schedule'; | ||||
|  | ||||
| @@ -10,6 +10,7 @@ export class AppService { | ||||
|     private jobService: JobService, | ||||
|     private searchService: SearchService, | ||||
|     private storageService: StorageService, | ||||
|     private serverService: ServerInfoService, | ||||
|   ) {} | ||||
|  | ||||
|   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) | ||||
| @@ -20,8 +21,6 @@ export class AppService { | ||||
|   async init() { | ||||
|     this.storageService.init(); | ||||
|     await this.searchService.init(); | ||||
|  | ||||
|     this.logger.log(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`); | ||||
|     this.logger.log(`Search is ${this.searchService.isEnabled() ? 'enabled' : 'disabled'}`); | ||||
|     this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,4 @@ | ||||
| import { | ||||
|   AuthUserDto, | ||||
|   SearchConfigResponseDto, | ||||
|   SearchDto, | ||||
|   SearchExploreResponseDto, | ||||
|   SearchResponseDto, | ||||
|   SearchService, | ||||
| } from '@app/domain'; | ||||
| import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain'; | ||||
| import { Controller, Get, Query } from '@nestjs/common'; | ||||
| import { ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated, AuthUser } from '../app.guard'; | ||||
| @@ -23,11 +16,6 @@ export class SearchController { | ||||
|     return this.service.search(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Get('config') | ||||
|   getSearchConfig(): SearchConfigResponseDto { | ||||
|     return this.service.getConfig(); | ||||
|   } | ||||
|  | ||||
|   @Get('explore') | ||||
|   getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> { | ||||
|     return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>; | ||||
|   | ||||
| @@ -37,6 +37,12 @@ export enum SystemConfigKey { | ||||
|   JOB_SEARCH_CONCURRENCY = 'job.search.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_ISSUER_URL = 'oauth.issuerUrl', | ||||
|   OAUTH_CLIENT_ID = 'oauth.clientId', | ||||
| @@ -105,6 +111,13 @@ export interface SystemConfig { | ||||
|     tonemap: ToneMapping; | ||||
|   }; | ||||
|   job: Record<QueueName, { concurrency: number }>; | ||||
|   machineLearning: { | ||||
|     enabled: boolean; | ||||
|     url: string; | ||||
|     clipEncodeEnabled: boolean; | ||||
|     facialRecognitionEnabled: boolean; | ||||
|     tagImageEnabled: boolean; | ||||
|   }; | ||||
|   oauth: { | ||||
|     enabled: boolean; | ||||
|     issuerUrl: string; | ||||
|   | ||||
| @@ -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 axios from 'axios'; | ||||
| import { createReadStream } from 'fs'; | ||||
|  | ||||
| const client = axios.create({ baseURL: MACHINE_LEARNING_URL }); | ||||
| const client = axios.create(); | ||||
|  | ||||
| @Injectable() | ||||
| 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); | ||||
|   } | ||||
|  | ||||
|   classifyImage(input: MachineLearningInput): Promise<string[]> { | ||||
|     return this.post<string[]>(input, '/image-classifier/tag-image'); | ||||
|   classifyImage(url: string, input: MachineLearningInput): Promise<string[]> { | ||||
|     return this.post<string[]>(input, `${url}/image-classifier/tag-image`); | ||||
|   } | ||||
|  | ||||
|   detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]> { | ||||
|     return this.post<DetectFaceResult[]>(input, '/facial-recognition/detect-faces'); | ||||
|   detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]> { | ||||
|     return this.post<DetectFaceResult[]>(input, `${url}/facial-recognition/detect-faces`); | ||||
|   } | ||||
|  | ||||
|   encodeImage(input: MachineLearningInput): Promise<number[]> { | ||||
|     return this.post<number[]>(input, '/sentence-transformer/encode-image'); | ||||
|   encodeImage(url: string, input: MachineLearningInput): Promise<number[]> { | ||||
|     return this.post<number[]>(input, `${url}/sentence-transformer/encode-image`); | ||||
|   } | ||||
|  | ||||
|   encodeText(input: string): Promise<number[]> { | ||||
|     return client.post<number[]>('/sentence-transformer/encode-text', { text: input }).then((res) => res.data); | ||||
|   encodeText(url: string, input: string): Promise<number[]> { | ||||
|     return client.post<number[]>(`${url}/sentence-transformer/encode-text`, { text: input }).then((res) => res.data); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										141
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto { | ||||
|      */ | ||||
|     'total': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SearchConfigResponseDto | ||||
|  */ | ||||
| export interface SearchConfigResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SearchConfigResponseDto | ||||
|      */ | ||||
|     'enabled': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto { | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'machineLearning': boolean; | ||||
|     'clipEncode': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'facialRecognition': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto { | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'search': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'sidecar': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'tagImage': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -2611,6 +2616,12 @@ export interface SystemConfigDto { | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'job': SystemConfigJobDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigMachineLearningDto} | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'machineLearning': SystemConfigMachineLearningDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigOAuthDto} | ||||
| @@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto { | ||||
|      */ | ||||
|     '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 | ||||
| @@ -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); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| @@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); | ||||
|             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]  | ||||
| @@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat | ||||
|         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { | ||||
|             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. | ||||
| @@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI { | ||||
|         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. | ||||
|   | ||||
| @@ -70,25 +70,26 @@ | ||||
|       subtitle: 'Discover or synchronize sidecar metadata from the filesystem', | ||||
|       allText: 'SYNC', | ||||
|       missingText: 'DISCOVER', | ||||
|       disabled: !$featureFlags.sidecar, | ||||
|     }, | ||||
|     [JobName.ObjectTagging]: { | ||||
|       icon: TagMultiple, | ||||
|       title: api.getJobName(JobName.ObjectTagging), | ||||
|       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]: { | ||||
|       icon: VectorCircle, | ||||
|       title: api.getJobName(JobName.ClipEncoding), | ||||
|       subtitle: 'Run machine learning to generate clip embeddings', | ||||
|       disabled: !$featureFlags.machineLearning, | ||||
|       disabled: !$featureFlags.clipEncode, | ||||
|     }, | ||||
|     [JobName.RecognizeFaces]: { | ||||
|       icon: FaceRecognition, | ||||
|       title: api.getJobName(JobName.RecognizeFaces), | ||||
|       subtitle: 'Run machine learning to recognize faces', | ||||
|       handleCommand: handleFaceCommand, | ||||
|       disabled: !$featureFlags.machineLearning, | ||||
|       disabled: !$featureFlags.facialRecognition, | ||||
|     }, | ||||
|     [JobName.VideoConversion]: { | ||||
|       icon: Video, | ||||
|   | ||||
| @@ -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> | ||||
| @@ -32,9 +32,9 @@ | ||||
|     <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} /> | ||||
|  | ||||
|     {#if disabled} | ||||
|       <span class="slider-disable" /> | ||||
|       <span class="slider-disable cursor-not-allowed" /> | ||||
|     {:else} | ||||
|       <span class="slider" /> | ||||
|       <span class="slider cursor-pointer" /> | ||||
|     {/if} | ||||
|   </label> | ||||
| </div> | ||||
| @@ -43,7 +43,6 @@ | ||||
|   .slider, | ||||
|   .slider-disable { | ||||
|     position: absolute; | ||||
|     cursor: pointer; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|   | ||||
| @@ -4,7 +4,10 @@ import { writable } from 'svelte/store'; | ||||
| export type FeatureFlags = ServerFeaturesDto; | ||||
|  | ||||
| export const featureFlags = writable<FeatureFlags>({ | ||||
|   machineLearning: true, | ||||
|   clipEncode: true, | ||||
|   facialRecognition: true, | ||||
|   sidecar: true, | ||||
|   tagImage: true, | ||||
|   search: true, | ||||
|   oauth: true, | ||||
|   oauthAutoLaunch: true, | ||||
|   | ||||
| @@ -2,11 +2,12 @@ | ||||
|   import { page } from '$app/stores'; | ||||
|   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 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 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 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 { api } from '@api'; | ||||
|   import type { PageData } from './$types'; | ||||
| @@ -50,6 +51,10 @@ | ||||
|       <OAuthSettings oauthConfig={configs.oauth} /> | ||||
|     </SettingAccordion> | ||||
|  | ||||
|     <SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings"> | ||||
|       <MachineLearningSettings /> | ||||
|     </SettingAccordion> | ||||
|  | ||||
|     <SettingAccordion | ||||
|       title="Storage Template" | ||||
|       subtitle="Manage the folder structure and file name of the upload asset" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user