mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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; |     'total': number; | ||||||
| } | } | ||||||
| /** |  | ||||||
|  *  |  | ||||||
|  * @export |  | ||||||
|  * @interface SearchConfigResponseDto |  | ||||||
|  */ |  | ||||||
| export interface SearchConfigResponseDto { |  | ||||||
|     /** |  | ||||||
|      *  |  | ||||||
|      * @type {boolean} |  | ||||||
|      * @memberof SearchConfigResponseDto |  | ||||||
|      */ |  | ||||||
|     'enabled': boolean; |  | ||||||
| } |  | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|  * @export |  * @export | ||||||
| @@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto { | |||||||
|      * @type {boolean} |      * @type {boolean} | ||||||
|      * @memberof ServerFeaturesDto |      * @memberof ServerFeaturesDto | ||||||
|      */ |      */ | ||||||
|     'machineLearning': boolean; |     'clipEncode': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'facialRecognition': boolean; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {boolean} |      * @type {boolean} | ||||||
| @@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto { | |||||||
|      * @memberof ServerFeaturesDto |      * @memberof ServerFeaturesDto | ||||||
|      */ |      */ | ||||||
|     'search': boolean; |     'search': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'sidecar': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'tagImage': boolean; | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
| @@ -2611,6 +2616,12 @@ export interface SystemConfigDto { | |||||||
|      * @memberof SystemConfigDto |      * @memberof SystemConfigDto | ||||||
|      */ |      */ | ||||||
|     'job': SystemConfigJobDto; |     'job': SystemConfigJobDto; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {SystemConfigMachineLearningDto} | ||||||
|  |      * @memberof SystemConfigDto | ||||||
|  |      */ | ||||||
|  |     'machineLearning': SystemConfigMachineLearningDto; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {SystemConfigOAuthDto} |      * @type {SystemConfigOAuthDto} | ||||||
| @@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto { | |||||||
|      */ |      */ | ||||||
|     'videoConversion': JobSettingsDto; |     'videoConversion': JobSettingsDto; | ||||||
| } | } | ||||||
|  | /** | ||||||
|  |  *  | ||||||
|  |  * @export | ||||||
|  |  * @interface SystemConfigMachineLearningDto | ||||||
|  |  */ | ||||||
|  | export interface SystemConfigMachineLearningDto { | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'clipEncodeEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'enabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'facialRecognitionEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'tagImageEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'url': string; | ||||||
|  | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|  * @export |  * @export | ||||||
| @@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); |  | ||||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; |  | ||||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; |  | ||||||
| 
 |  | ||||||
|             return { |  | ||||||
|                 url: toPathString(localVarUrlObj), |  | ||||||
|                 options: localVarRequestOptions, |  | ||||||
|             }; |  | ||||||
|         }, |  | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |  | ||||||
|             const localVarPath = `/search/config`; |  | ||||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 |  | ||||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); |  | ||||||
|             let baseOptions; |  | ||||||
|             if (configuration) { |  | ||||||
|                 baseOptions = configuration.baseOptions; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; |  | ||||||
|             const localVarHeaderParameter = {} as any; |  | ||||||
|             const localVarQueryParameter = {} as any; |  | ||||||
| 
 |  | ||||||
|             // authentication cookie required
 |  | ||||||
| 
 |  | ||||||
|             // authentication api_key required
 |  | ||||||
|             await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) |  | ||||||
| 
 |  | ||||||
|             // authentication bearer required
 |  | ||||||
|             // http bearer authentication required
 |  | ||||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); |             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; |             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; |             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||||
| @@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) { | |||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> { |  | ||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options); |  | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |  | ||||||
|         }, |  | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
|          * @param {string} [q]  |          * @param {string} [q]  | ||||||
| @@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat | |||||||
|         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { |         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { | ||||||
|             return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); |             return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> { |  | ||||||
|             return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath)); |  | ||||||
|         }, |  | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
|          * @param {SearchApiSearchRequest} requestParameters Request parameters. |          * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||||
| @@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI { | |||||||
|         return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); |         return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      *  |  | ||||||
|      * @param {*} [options] Override http request option. |  | ||||||
|      * @throws {RequiredError} |  | ||||||
|      * @memberof SearchApi |  | ||||||
|      */ |  | ||||||
|     public getSearchConfig(options?: AxiosRequestConfig) { |  | ||||||
|         return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @param {SearchApiSearchRequest} requestParameters Request parameters. |      * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ This often happens when using a reverse proxy or cloudflare tunnel in front of I | |||||||
|  |  | ||||||
| ### Why is Immich slow on low-memory systems like the Raspberry Pi? | ### Why is Immich slow on low-memory systems like the Raspberry Pi? | ||||||
|  |  | ||||||
| Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file. | Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_ENABLED=false` in your .env file. | ||||||
|  |  | ||||||
| ### How to disable machine-learning and TypeSense? | ### How to disable machine-learning and TypeSense? | ||||||
|  |  | ||||||
| @@ -47,7 +47,7 @@ Immich uses optional machine-learning features to enhance search results. This f | |||||||
| Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning. | Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning. | ||||||
| ::: | ::: | ||||||
|  |  | ||||||
| These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_URL=false` & `TYPESENSE_ENABLED=false` in your .env file. | These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_ENABLED=false` & `TYPESENSE_ENABLED=false` in your .env file. | ||||||
|  |  | ||||||
| ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)? | ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)? | ||||||
|  |  | ||||||
|   | |||||||
| @@ -132,7 +132,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server" | |||||||
|  |  | ||||||
| IMMICH_WEB_URL=http://immich-web:3000 | IMMICH_WEB_URL=http://immich-web:3000 | ||||||
| IMMICH_SERVER_URL=http://immich-server:3001 | IMMICH_SERVER_URL=http://immich-server:3001 | ||||||
| IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003 |  | ||||||
|  |  | ||||||
| #################################################################################### | #################################################################################### | ||||||
| # Alternative API's External Address - Optional | # Alternative API's External Address - Optional | ||||||
|   | |||||||
| @@ -50,13 +50,14 @@ These environment variables are used by the `docker-compose.yml` file and do **N | |||||||
|  |  | ||||||
| ## URLs | ## URLs | ||||||
|  |  | ||||||
| | Variable                      | Description                                              |                Default                | Services              | | | Variable                          | Description                  |                Default                | Services              | | ||||||
| | :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- | | | :-------------------------------- | :--------------------------- | :-----------------------------------: | :-------------------- | | ||||||
| | `IMMICH_WEB_URL`              | Immich Web URL                                           |       `http://immich-web:3000`        | proxy                 | | | `IMMICH_WEB_URL`                  | Immich Web URL               |       `http://immich-web:3000`        | proxy                 | | ||||||
| | `IMMICH_SERVER_URL`           | Immich Server URL                                        |      `http://immich-server:3001`      | web, proxy            | | | `IMMICH_SERVER_URL`               | Immich Server URL            |      `http://immich-server:3001`      | web, proxy            | | ||||||
| | `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, set `"false"` to disable ML | `http://immich-machine-learning:3003` | server, microservices | | | `IMMICH_MACHINE_LEARNING_ENABLED` | Enabled machine learning     |                `true`                 | server, microservices | | ||||||
| | `PUBLIC_IMMICH_SERVER_URL`    | Public Immich URL                                        |      `http://immich-server:3001`      | web                   | | | `IMMICH_MACHINE_LEARNING_URL`     | Immich Machine Learning URL, | `http://immich-machine-learning:3003` | server, microservices | | ||||||
| | `IMMICH_API_URL_EXTERNAL`     | Immich API URL External                                  |                `/api`                 | web                   | | | `PUBLIC_IMMICH_SERVER_URL`        | Public Immich URL            |      `http://immich-server:3001`      | web                   | | ||||||
|  | | `IMMICH_API_URL_EXTERNAL`         | Immich API URL External      |                `/api`                 | web                   | | ||||||
|  |  | ||||||
| :::info | :::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/SearchApi.md | ||||||
| doc/SearchAssetDto.md | doc/SearchAssetDto.md | ||||||
| doc/SearchAssetResponseDto.md | doc/SearchAssetResponseDto.md | ||||||
| doc/SearchConfigResponseDto.md |  | ||||||
| doc/SearchExploreItem.md | doc/SearchExploreItem.md | ||||||
| doc/SearchExploreResponseDto.md | doc/SearchExploreResponseDto.md | ||||||
| doc/SearchFacetCountResponseDto.md | doc/SearchFacetCountResponseDto.md | ||||||
| @@ -108,6 +107,7 @@ doc/SystemConfigApi.md | |||||||
| doc/SystemConfigDto.md | doc/SystemConfigDto.md | ||||||
| doc/SystemConfigFFmpegDto.md | doc/SystemConfigFFmpegDto.md | ||||||
| doc/SystemConfigJobDto.md | doc/SystemConfigJobDto.md | ||||||
|  | doc/SystemConfigMachineLearningDto.md | ||||||
| doc/SystemConfigOAuthDto.md | doc/SystemConfigOAuthDto.md | ||||||
| doc/SystemConfigPasswordLoginDto.md | doc/SystemConfigPasswordLoginDto.md | ||||||
| doc/SystemConfigStorageTemplateDto.md | doc/SystemConfigStorageTemplateDto.md | ||||||
| @@ -228,7 +228,6 @@ lib/model/queue_status_dto.dart | |||||||
| lib/model/search_album_response_dto.dart | lib/model/search_album_response_dto.dart | ||||||
| lib/model/search_asset_dto.dart | lib/model/search_asset_dto.dart | ||||||
| lib/model/search_asset_response_dto.dart | lib/model/search_asset_response_dto.dart | ||||||
| lib/model/search_config_response_dto.dart |  | ||||||
| lib/model/search_explore_item.dart | lib/model/search_explore_item.dart | ||||||
| lib/model/search_explore_response_dto.dart | lib/model/search_explore_response_dto.dart | ||||||
| lib/model/search_facet_count_response_dto.dart | lib/model/search_facet_count_response_dto.dart | ||||||
| @@ -249,6 +248,7 @@ lib/model/smart_info_response_dto.dart | |||||||
| lib/model/system_config_dto.dart | lib/model/system_config_dto.dart | ||||||
| lib/model/system_config_f_fmpeg_dto.dart | lib/model/system_config_f_fmpeg_dto.dart | ||||||
| lib/model/system_config_job_dto.dart | lib/model/system_config_job_dto.dart | ||||||
|  | lib/model/system_config_machine_learning_dto.dart | ||||||
| lib/model/system_config_o_auth_dto.dart | lib/model/system_config_o_auth_dto.dart | ||||||
| lib/model/system_config_password_login_dto.dart | lib/model/system_config_password_login_dto.dart | ||||||
| lib/model/system_config_storage_template_dto.dart | lib/model/system_config_storage_template_dto.dart | ||||||
| @@ -353,7 +353,6 @@ test/search_album_response_dto_test.dart | |||||||
| test/search_api_test.dart | test/search_api_test.dart | ||||||
| test/search_asset_dto_test.dart | test/search_asset_dto_test.dart | ||||||
| test/search_asset_response_dto_test.dart | test/search_asset_response_dto_test.dart | ||||||
| test/search_config_response_dto_test.dart |  | ||||||
| test/search_explore_item_test.dart | test/search_explore_item_test.dart | ||||||
| test/search_explore_response_dto_test.dart | test/search_explore_response_dto_test.dart | ||||||
| test/search_facet_count_response_dto_test.dart | test/search_facet_count_response_dto_test.dart | ||||||
| @@ -377,6 +376,7 @@ test/system_config_api_test.dart | |||||||
| test/system_config_dto_test.dart | test/system_config_dto_test.dart | ||||||
| test/system_config_f_fmpeg_dto_test.dart | test/system_config_f_fmpeg_dto_test.dart | ||||||
| test/system_config_job_dto_test.dart | test/system_config_job_dto_test.dart | ||||||
|  | test/system_config_machine_learning_dto_test.dart | ||||||
| test/system_config_o_auth_dto_test.dart | test/system_config_o_auth_dto_test.dart | ||||||
| test/system_config_password_login_dto_test.dart | test/system_config_password_login_dto_test.dart | ||||||
| test/system_config_storage_template_dto_test.dart | test/system_config_storage_template_dto_test.dart | ||||||
|   | |||||||
							
								
								
									
										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* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |  | ||||||
| *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |  | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |  | ||||||
| *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |  | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |  | ||||||
| *SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config |  |  | ||||||
| *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |  | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |  | ||||||
| *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |  | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |  | ||||||
| *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |  | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |  | ||||||
| @@ -253,7 +252,6 @@ Class | Method | HTTP request | Description | |||||||
|  - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) |  - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) | ||||||
|  - [SearchAssetDto](doc//SearchAssetDto.md) |  - [SearchAssetDto](doc//SearchAssetDto.md) | ||||||
|  - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) |  - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) | ||||||
|  - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md) |  | ||||||
|  - [SearchExploreItem](doc//SearchExploreItem.md) |  - [SearchExploreItem](doc//SearchExploreItem.md) | ||||||
|  - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md) |  - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md) | ||||||
|  - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) |  - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) | ||||||
| @@ -274,6 +272,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [SystemConfigDto](doc//SystemConfigDto.md) |  - [SystemConfigDto](doc//SystemConfigDto.md) | ||||||
|  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) |  - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) | ||||||
|  - [SystemConfigJobDto](doc//SystemConfigJobDto.md) |  - [SystemConfigJobDto](doc//SystemConfigJobDto.md) | ||||||
|  |  - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md) | ||||||
|  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) |  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) | ||||||
|  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) |  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) | ||||||
|  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) |  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) | ||||||
|   | |||||||
							
								
								
									
										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 | Method | HTTP request | Description | ||||||
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ||||||
| [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |  | [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |  | ||||||
| [**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config |  |  | ||||||
| [**search**](SearchApi.md#search) | **GET** /search |  | [**search**](SearchApi.md#search) | **GET** /search |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -65,57 +64,6 @@ This endpoint does not need any parameter. | |||||||
| 
 | 
 | ||||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
| # **getSearchConfig** |  | ||||||
| > SearchConfigResponseDto getSearchConfig() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ### Example |  | ||||||
| ```dart |  | ||||||
| import 'package:openapi/api.dart'; |  | ||||||
| // TODO Configure API key authorization: cookie |  | ||||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; |  | ||||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed |  | ||||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; |  | ||||||
| // TODO Configure API key authorization: api_key |  | ||||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; |  | ||||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed |  | ||||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; |  | ||||||
| // TODO Configure HTTP Bearer authorization: bearer |  | ||||||
| // Case 1. Use String Token |  | ||||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); |  | ||||||
| // Case 2. Use Function which generate token. |  | ||||||
| // String yourTokenGeneratorFunction() { ... } |  | ||||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); |  | ||||||
| 
 |  | ||||||
| final api_instance = SearchApi(); |  | ||||||
| 
 |  | ||||||
| try { |  | ||||||
|     final result = api_instance.getSearchConfig(); |  | ||||||
|     print(result); |  | ||||||
| } catch (e) { |  | ||||||
|     print('Exception when calling SearchApi->getSearchConfig: $e\n'); |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Parameters |  | ||||||
| This endpoint does not need any parameter. |  | ||||||
| 
 |  | ||||||
| ### Return type |  | ||||||
| 
 |  | ||||||
| [**SearchConfigResponseDto**](SearchConfigResponseDto.md) |  | ||||||
| 
 |  | ||||||
| ### Authorization |  | ||||||
| 
 |  | ||||||
| [cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) |  | ||||||
| 
 |  | ||||||
| ### HTTP request headers |  | ||||||
| 
 |  | ||||||
|  - **Content-Type**: Not defined |  | ||||||
|  - **Accept**: application/json |  | ||||||
| 
 |  | ||||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) |  | ||||||
| 
 |  | ||||||
| # **search** | # **search** | ||||||
| > SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion) | > SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion) | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/doc/ServerFeaturesDto.md
									
									
									
										generated
									
									
									
								
							| @@ -8,11 +8,14 @@ import 'package:openapi/api.dart'; | |||||||
| ## Properties | ## Properties | ||||||
| Name | Type | Description | Notes | Name | Type | Description | Notes | ||||||
| ------------ | ------------- | ------------- | ------------- | ------------ | ------------- | ------------- | ------------- | ||||||
| **machineLearning** | **bool** |  |  | **clipEncode** | **bool** |  |  | ||||||
|  | **facialRecognition** | **bool** |  |  | ||||||
| **oauth** | **bool** |  |  | **oauth** | **bool** |  |  | ||||||
| **oauthAutoLaunch** | **bool** |  |  | **oauthAutoLaunch** | **bool** |  |  | ||||||
| **passwordLogin** | **bool** |  |  | **passwordLogin** | **bool** |  |  | ||||||
| **search** | **bool** |  |  | **search** | **bool** |  |  | ||||||
|  | **sidecar** | **bool** |  |  | ||||||
|  | **tagImage** | **bool** |  |  | ||||||
| 
 | 
 | ||||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										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) |  |  | **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) |  |  | ||||||
| **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  |  | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) |  |  | ||||||
|  | **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) |  |  | ||||||
| **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  |  | **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  |  | ||||||
| **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  |  | **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  |  | ||||||
| **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  |  | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| # openapi.model.SearchConfigResponseDto | # openapi.model.SystemConfigMachineLearningDto | ||||||
| 
 | 
 | ||||||
| ## Load the model package | ## Load the model package | ||||||
| ```dart | ```dart | ||||||
| @@ -8,7 +8,11 @@ import 'package:openapi/api.dart'; | |||||||
| ## Properties | ## Properties | ||||||
| Name | Type | Description | Notes | Name | Type | Description | Notes | ||||||
| ------------ | ------------- | ------------- | ------------- | ------------ | ------------- | ------------- | ------------- | ||||||
|  | **clipEncodeEnabled** | **bool** |  |  | ||||||
| **enabled** | **bool** |  |  | **enabled** | **bool** |  |  | ||||||
|  | **facialRecognitionEnabled** | **bool** |  |  | ||||||
|  | **tagImageEnabled** | **bool** |  |  | ||||||
|  | **url** | **String** |  |  | ||||||
| 
 | 
 | ||||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
							
								
								
									
										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_album_response_dto.dart'; | ||||||
| part 'model/search_asset_dto.dart'; | part 'model/search_asset_dto.dart'; | ||||||
| part 'model/search_asset_response_dto.dart'; | part 'model/search_asset_response_dto.dart'; | ||||||
| part 'model/search_config_response_dto.dart'; |  | ||||||
| part 'model/search_explore_item.dart'; | part 'model/search_explore_item.dart'; | ||||||
| part 'model/search_explore_response_dto.dart'; | part 'model/search_explore_response_dto.dart'; | ||||||
| part 'model/search_facet_count_response_dto.dart'; | part 'model/search_facet_count_response_dto.dart'; | ||||||
| @@ -136,6 +135,7 @@ part 'model/smart_info_response_dto.dart'; | |||||||
| part 'model/system_config_dto.dart'; | part 'model/system_config_dto.dart'; | ||||||
| part 'model/system_config_f_fmpeg_dto.dart'; | part 'model/system_config_f_fmpeg_dto.dart'; | ||||||
| part 'model/system_config_job_dto.dart'; | part 'model/system_config_job_dto.dart'; | ||||||
|  | part 'model/system_config_machine_learning_dto.dart'; | ||||||
| part 'model/system_config_o_auth_dto.dart'; | part 'model/system_config_o_auth_dto.dart'; | ||||||
| part 'model/system_config_password_login_dto.dart'; | part 'model/system_config_password_login_dto.dart'; | ||||||
| part 'model/system_config_storage_template_dto.dart'; | part 'model/system_config_storage_template_dto.dart'; | ||||||
|   | |||||||
							
								
								
									
										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; |     return null; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Performs an HTTP 'GET /search/config' operation and returns the [Response]. |  | ||||||
|   Future<Response> getSearchConfigWithHttpInfo() async { |  | ||||||
|     // ignore: prefer_const_declarations |  | ||||||
|     final path = r'/search/config'; |  | ||||||
| 
 |  | ||||||
|     // ignore: prefer_final_locals |  | ||||||
|     Object? postBody; |  | ||||||
| 
 |  | ||||||
|     final queryParams = <QueryParam>[]; |  | ||||||
|     final headerParams = <String, String>{}; |  | ||||||
|     final formParams = <String, String>{}; |  | ||||||
| 
 |  | ||||||
|     const contentTypes = <String>[]; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     return apiClient.invokeAPI( |  | ||||||
|       path, |  | ||||||
|       'GET', |  | ||||||
|       queryParams, |  | ||||||
|       postBody, |  | ||||||
|       headerParams, |  | ||||||
|       formParams, |  | ||||||
|       contentTypes.isEmpty ? null : contentTypes.first, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Future<SearchConfigResponseDto?> getSearchConfig() async { |  | ||||||
|     final response = await getSearchConfigWithHttpInfo(); |  | ||||||
|     if (response.statusCode >= HttpStatus.badRequest) { |  | ||||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); |  | ||||||
|     } |  | ||||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. |  | ||||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" |  | ||||||
|     // FormatException when trying to decode an empty string. |  | ||||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { |  | ||||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchConfigResponseDto',) as SearchConfigResponseDto; |  | ||||||
|      |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Performs an HTTP 'GET /search' operation and returns the [Response]. |   /// Performs an HTTP 'GET /search' operation and returns the [Response]. | ||||||
|   /// Parameters: |   /// Parameters: | ||||||
|   /// |   /// | ||||||
|   | |||||||
							
								
								
									
										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); |           return SearchAssetDto.fromJson(value); | ||||||
|         case 'SearchAssetResponseDto': |         case 'SearchAssetResponseDto': | ||||||
|           return SearchAssetResponseDto.fromJson(value); |           return SearchAssetResponseDto.fromJson(value); | ||||||
|         case 'SearchConfigResponseDto': |  | ||||||
|           return SearchConfigResponseDto.fromJson(value); |  | ||||||
|         case 'SearchExploreItem': |         case 'SearchExploreItem': | ||||||
|           return SearchExploreItem.fromJson(value); |           return SearchExploreItem.fromJson(value); | ||||||
|         case 'SearchExploreResponseDto': |         case 'SearchExploreResponseDto': | ||||||
| @@ -365,6 +363,8 @@ class ApiClient { | |||||||
|           return SystemConfigFFmpegDto.fromJson(value); |           return SystemConfigFFmpegDto.fromJson(value); | ||||||
|         case 'SystemConfigJobDto': |         case 'SystemConfigJobDto': | ||||||
|           return SystemConfigJobDto.fromJson(value); |           return SystemConfigJobDto.fromJson(value); | ||||||
|  |         case 'SystemConfigMachineLearningDto': | ||||||
|  |           return SystemConfigMachineLearningDto.fromJson(value); | ||||||
|         case 'SystemConfigOAuthDto': |         case 'SystemConfigOAuthDto': | ||||||
|           return SystemConfigOAuthDto.fromJson(value); |           return SystemConfigOAuthDto.fromJson(value); | ||||||
|         case 'SystemConfigPasswordLoginDto': |         case 'SystemConfigPasswordLoginDto': | ||||||
|   | |||||||
| @@ -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 { | class ServerFeaturesDto { | ||||||
|   /// Returns a new [ServerFeaturesDto] instance. |   /// Returns a new [ServerFeaturesDto] instance. | ||||||
|   ServerFeaturesDto({ |   ServerFeaturesDto({ | ||||||
|     required this.machineLearning, |     required this.clipEncode, | ||||||
|  |     required this.facialRecognition, | ||||||
|     required this.oauth, |     required this.oauth, | ||||||
|     required this.oauthAutoLaunch, |     required this.oauthAutoLaunch, | ||||||
|     required this.passwordLogin, |     required this.passwordLogin, | ||||||
|     required this.search, |     required this.search, | ||||||
|  |     required this.sidecar, | ||||||
|  |     required this.tagImage, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   bool machineLearning; |   bool clipEncode; | ||||||
|  | 
 | ||||||
|  |   bool facialRecognition; | ||||||
| 
 | 
 | ||||||
|   bool oauth; |   bool oauth; | ||||||
| 
 | 
 | ||||||
| @@ -30,33 +35,46 @@ class ServerFeaturesDto { | |||||||
| 
 | 
 | ||||||
|   bool search; |   bool search; | ||||||
| 
 | 
 | ||||||
|  |   bool sidecar; | ||||||
|  | 
 | ||||||
|  |   bool tagImage; | ||||||
|  | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto && |   bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto && | ||||||
|      other.machineLearning == machineLearning && |      other.clipEncode == clipEncode && | ||||||
|  |      other.facialRecognition == facialRecognition && | ||||||
|      other.oauth == oauth && |      other.oauth == oauth && | ||||||
|      other.oauthAutoLaunch == oauthAutoLaunch && |      other.oauthAutoLaunch == oauthAutoLaunch && | ||||||
|      other.passwordLogin == passwordLogin && |      other.passwordLogin == passwordLogin && | ||||||
|      other.search == search; |      other.search == search && | ||||||
|  |      other.sidecar == sidecar && | ||||||
|  |      other.tagImage == tagImage; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
|     // ignore: unnecessary_parenthesis |     // ignore: unnecessary_parenthesis | ||||||
|     (machineLearning.hashCode) + |     (clipEncode.hashCode) + | ||||||
|  |     (facialRecognition.hashCode) + | ||||||
|     (oauth.hashCode) + |     (oauth.hashCode) + | ||||||
|     (oauthAutoLaunch.hashCode) + |     (oauthAutoLaunch.hashCode) + | ||||||
|     (passwordLogin.hashCode) + |     (passwordLogin.hashCode) + | ||||||
|     (search.hashCode); |     (search.hashCode) + | ||||||
|  |     (sidecar.hashCode) + | ||||||
|  |     (tagImage.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'ServerFeaturesDto[machineLearning=$machineLearning, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search]'; |   String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, facialRecognition=$facialRecognition, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
|       json[r'machineLearning'] = this.machineLearning; |       json[r'clipEncode'] = this.clipEncode; | ||||||
|  |       json[r'facialRecognition'] = this.facialRecognition; | ||||||
|       json[r'oauth'] = this.oauth; |       json[r'oauth'] = this.oauth; | ||||||
|       json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; |       json[r'oauthAutoLaunch'] = this.oauthAutoLaunch; | ||||||
|       json[r'passwordLogin'] = this.passwordLogin; |       json[r'passwordLogin'] = this.passwordLogin; | ||||||
|       json[r'search'] = this.search; |       json[r'search'] = this.search; | ||||||
|  |       json[r'sidecar'] = this.sidecar; | ||||||
|  |       json[r'tagImage'] = this.tagImage; | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -68,11 +86,14 @@ class ServerFeaturesDto { | |||||||
|       final json = value.cast<String, dynamic>(); |       final json = value.cast<String, dynamic>(); | ||||||
| 
 | 
 | ||||||
|       return ServerFeaturesDto( |       return ServerFeaturesDto( | ||||||
|         machineLearning: mapValueOfType<bool>(json, r'machineLearning')!, |         clipEncode: mapValueOfType<bool>(json, r'clipEncode')!, | ||||||
|  |         facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!, | ||||||
|         oauth: mapValueOfType<bool>(json, r'oauth')!, |         oauth: mapValueOfType<bool>(json, r'oauth')!, | ||||||
|         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!, |         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!, | ||||||
|         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!, |         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!, | ||||||
|         search: mapValueOfType<bool>(json, r'search')!, |         search: mapValueOfType<bool>(json, r'search')!, | ||||||
|  |         sidecar: mapValueOfType<bool>(json, r'sidecar')!, | ||||||
|  |         tagImage: mapValueOfType<bool>(json, r'tagImage')!, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
| @@ -120,11 +141,14 @@ class ServerFeaturesDto { | |||||||
| 
 | 
 | ||||||
|   /// The list of required keys that must be present in a JSON. |   /// The list of required keys that must be present in a JSON. | ||||||
|   static const requiredKeys = <String>{ |   static const requiredKeys = <String>{ | ||||||
|     'machineLearning', |     'clipEncode', | ||||||
|  |     'facialRecognition', | ||||||
|     'oauth', |     'oauth', | ||||||
|     'oauthAutoLaunch', |     'oauthAutoLaunch', | ||||||
|     'passwordLogin', |     'passwordLogin', | ||||||
|     'search', |     'search', | ||||||
|  |     'sidecar', | ||||||
|  |     'tagImage', | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										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({ |   SystemConfigDto({ | ||||||
|     required this.ffmpeg, |     required this.ffmpeg, | ||||||
|     required this.job, |     required this.job, | ||||||
|  |     required this.machineLearning, | ||||||
|     required this.oauth, |     required this.oauth, | ||||||
|     required this.passwordLogin, |     required this.passwordLogin, | ||||||
|     required this.storageTemplate, |     required this.storageTemplate, | ||||||
| @@ -25,6 +26,8 @@ class SystemConfigDto { | |||||||
| 
 | 
 | ||||||
|   SystemConfigJobDto job; |   SystemConfigJobDto job; | ||||||
| 
 | 
 | ||||||
|  |   SystemConfigMachineLearningDto machineLearning; | ||||||
|  | 
 | ||||||
|   SystemConfigOAuthDto oauth; |   SystemConfigOAuthDto oauth; | ||||||
| 
 | 
 | ||||||
|   SystemConfigPasswordLoginDto passwordLogin; |   SystemConfigPasswordLoginDto passwordLogin; | ||||||
| @@ -37,6 +40,7 @@ class SystemConfigDto { | |||||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && |   bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && | ||||||
|      other.ffmpeg == ffmpeg && |      other.ffmpeg == ffmpeg && | ||||||
|      other.job == job && |      other.job == job && | ||||||
|  |      other.machineLearning == machineLearning && | ||||||
|      other.oauth == oauth && |      other.oauth == oauth && | ||||||
|      other.passwordLogin == passwordLogin && |      other.passwordLogin == passwordLogin && | ||||||
|      other.storageTemplate == storageTemplate && |      other.storageTemplate == storageTemplate && | ||||||
| @@ -47,18 +51,20 @@ class SystemConfigDto { | |||||||
|     // ignore: unnecessary_parenthesis |     // ignore: unnecessary_parenthesis | ||||||
|     (ffmpeg.hashCode) + |     (ffmpeg.hashCode) + | ||||||
|     (job.hashCode) + |     (job.hashCode) + | ||||||
|  |     (machineLearning.hashCode) + | ||||||
|     (oauth.hashCode) + |     (oauth.hashCode) + | ||||||
|     (passwordLogin.hashCode) + |     (passwordLogin.hashCode) + | ||||||
|     (storageTemplate.hashCode) + |     (storageTemplate.hashCode) + | ||||||
|     (thumbnail.hashCode); |     (thumbnail.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]'; |   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
|       json[r'ffmpeg'] = this.ffmpeg; |       json[r'ffmpeg'] = this.ffmpeg; | ||||||
|       json[r'job'] = this.job; |       json[r'job'] = this.job; | ||||||
|  |       json[r'machineLearning'] = this.machineLearning; | ||||||
|       json[r'oauth'] = this.oauth; |       json[r'oauth'] = this.oauth; | ||||||
|       json[r'passwordLogin'] = this.passwordLogin; |       json[r'passwordLogin'] = this.passwordLogin; | ||||||
|       json[r'storageTemplate'] = this.storageTemplate; |       json[r'storageTemplate'] = this.storageTemplate; | ||||||
| @@ -76,6 +82,7 @@ class SystemConfigDto { | |||||||
|       return SystemConfigDto( |       return SystemConfigDto( | ||||||
|         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, |         ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, | ||||||
|         job: SystemConfigJobDto.fromJson(json[r'job'])!, |         job: SystemConfigJobDto.fromJson(json[r'job'])!, | ||||||
|  |         machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!, | ||||||
|         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, |         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, | ||||||
|         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, |         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, | ||||||
|         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, |         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, | ||||||
| @@ -129,6 +136,7 @@ class SystemConfigDto { | |||||||
|   static const requiredKeys = <String>{ |   static const requiredKeys = <String>{ | ||||||
|     'ffmpeg', |     'ffmpeg', | ||||||
|     'job', |     'job', | ||||||
|  |     'machineLearning', | ||||||
|     'oauth', |     'oauth', | ||||||
|     'passwordLogin', |     'passwordLogin', | ||||||
|     'storageTemplate', |     'storageTemplate', | ||||||
|   | |||||||
							
								
								
									
										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 |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     //Future<SearchConfigResponseDto> getSearchConfig() async |  | ||||||
|     test('test getSearchConfig', () async { |  | ||||||
|       // TODO |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async |     //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async | ||||||
|     test('test search', () async { |     test('test search', () async { | ||||||
|       // TODO |       // TODO | ||||||
|   | |||||||
| @@ -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(); |   // final instance = ServerFeaturesDto(); | ||||||
| 
 | 
 | ||||||
|   group('test ServerFeaturesDto', () { |   group('test ServerFeaturesDto', () { | ||||||
|     // bool machineLearning |     // bool clipEncode | ||||||
|     test('to test the property `machineLearning`', () async { |     test('to test the property `clipEncode`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool facialRecognition | ||||||
|  |     test('to test the property `facialRecognition`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @@ -41,6 +46,16 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // bool sidecar | ||||||
|  |     test('to test the property `sidecar`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // bool tagImage | ||||||
|  |     test('to test the property `tagImage`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										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 |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // SystemConfigMachineLearningDto machineLearning | ||||||
|  |     test('to test the property `machineLearning`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // SystemConfigOAuthDto oauth |     // SystemConfigOAuthDto oauth | ||||||
|     test('to test the property `oauth`', () async { |     test('to test the property `oauth`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|   | |||||||
							
								
								
									
										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": { |     "/search/explore": { | ||||||
|       "get": { |       "get": { | ||||||
|         "operationId": "getExploreData", |         "operationId": "getExploreData", | ||||||
| @@ -6424,17 +6392,6 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|       "SearchConfigResponseDto": { |  | ||||||
|         "properties": { |  | ||||||
|           "enabled": { |  | ||||||
|             "type": "boolean" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         "required": [ |  | ||||||
|           "enabled" |  | ||||||
|         ], |  | ||||||
|         "type": "object" |  | ||||||
|       }, |  | ||||||
|       "SearchExploreItem": { |       "SearchExploreItem": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "data": { |           "data": { | ||||||
| @@ -6518,7 +6475,10 @@ | |||||||
|       }, |       }, | ||||||
|       "ServerFeaturesDto": { |       "ServerFeaturesDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "machineLearning": { |           "clipEncode": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "facialRecognition": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|           }, |           }, | ||||||
|           "oauth": { |           "oauth": { | ||||||
| @@ -6532,11 +6492,20 @@ | |||||||
|           }, |           }, | ||||||
|           "search": { |           "search": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "sidecar": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "tagImage": { | ||||||
|  |             "type": "boolean" | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "required": [ |         "required": [ | ||||||
|           "machineLearning", |           "clipEncode", | ||||||
|  |           "facialRecognition", | ||||||
|  |           "sidecar", | ||||||
|           "search", |           "search", | ||||||
|  |           "tagImage", | ||||||
|           "oauth", |           "oauth", | ||||||
|           "oauthAutoLaunch", |           "oauthAutoLaunch", | ||||||
|           "passwordLogin" |           "passwordLogin" | ||||||
| @@ -6868,6 +6837,9 @@ | |||||||
|           "job": { |           "job": { | ||||||
|             "$ref": "#/components/schemas/SystemConfigJobDto" |             "$ref": "#/components/schemas/SystemConfigJobDto" | ||||||
|           }, |           }, | ||||||
|  |           "machineLearning": { | ||||||
|  |             "$ref": "#/components/schemas/SystemConfigMachineLearningDto" | ||||||
|  |           }, | ||||||
|           "oauth": { |           "oauth": { | ||||||
|             "$ref": "#/components/schemas/SystemConfigOAuthDto" |             "$ref": "#/components/schemas/SystemConfigOAuthDto" | ||||||
|           }, |           }, | ||||||
| @@ -6883,6 +6855,7 @@ | |||||||
|         }, |         }, | ||||||
|         "required": [ |         "required": [ | ||||||
|           "ffmpeg", |           "ffmpeg", | ||||||
|  |           "machineLearning", | ||||||
|           "oauth", |           "oauth", | ||||||
|           "passwordLogin", |           "passwordLogin", | ||||||
|           "storageTemplate", |           "storageTemplate", | ||||||
| @@ -6989,6 +6962,33 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|  |       "SystemConfigMachineLearningDto": { | ||||||
|  |         "properties": { | ||||||
|  |           "clipEncodeEnabled": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "enabled": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "facialRecognitionEnabled": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "tagImageEnabled": { | ||||||
|  |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "url": { | ||||||
|  |             "type": "string" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "required": [ | ||||||
|  |           "enabled", | ||||||
|  |           "url", | ||||||
|  |           "clipEncodeEnabled", | ||||||
|  |           "facialRecognitionEnabled", | ||||||
|  |           "tagImageEnabled" | ||||||
|  |         ], | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "SystemConfigOAuthDto": { |       "SystemConfigOAuthDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "autoLaunch": { |           "autoLaunch": { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { AssetType } from '@app/infra/entities'; | import { AssetType } from '@app/infra/entities'; | ||||||
| import { BadRequestException } from '@nestjs/common'; |  | ||||||
| import { Duration } from 'luxon'; | import { Duration } from 'luxon'; | ||||||
| import { extname } from 'node:path'; | import { extname } from 'node:path'; | ||||||
| import pkg from 'src/../../package.json'; | import pkg from 'src/../../package.json'; | ||||||
| @@ -24,17 +23,6 @@ export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${s | |||||||
|  |  | ||||||
| export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; | export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; | ||||||
|  |  | ||||||
| export const SEARCH_ENABLED = process.env.TYPESENSE_ENABLED !== 'false'; |  | ||||||
|  |  | ||||||
| export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'; |  | ||||||
| export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false'; |  | ||||||
|  |  | ||||||
| export function assertMachineLearningEnabled() { |  | ||||||
|   if (!MACHINE_LEARNING_ENABLED) { |  | ||||||
|     throw new BadRequestException('Machine learning is not enabled.'); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const image: Record<string, string[]> = { | const image: Record<string, string[]> = { | ||||||
|   '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], |   '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], | ||||||
|   '.ari': ['image/ari', 'image/x-arriflex-ari'], |   '.ari': ['image/ari', 'image/x-arriflex-ari'], | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import { | |||||||
|   newPersonRepositoryMock, |   newPersonRepositoryMock, | ||||||
|   newSearchRepositoryMock, |   newSearchRepositoryMock, | ||||||
|   newStorageRepositoryMock, |   newStorageRepositoryMock, | ||||||
|  |   newSystemConfigRepositoryMock, | ||||||
|   personStub, |   personStub, | ||||||
| } from '@test'; | } from '@test'; | ||||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | import { IAssetRepository, WithoutProperty } from '../asset'; | ||||||
| @@ -18,6 +19,7 @@ import { IPersonRepository } from '../person'; | |||||||
| import { ISearchRepository } from '../search'; | import { ISearchRepository } from '../search'; | ||||||
| import { IMachineLearningRepository } from '../smart-info'; | import { IMachineLearningRepository } from '../smart-info'; | ||||||
| import { IStorageRepository } from '../storage'; | import { IStorageRepository } from '../storage'; | ||||||
|  | import { ISystemConfigRepository } from '../system-config'; | ||||||
| import { IFaceRepository } from './face.repository'; | import { IFaceRepository } from './face.repository'; | ||||||
| import { FacialRecognitionService } from './facial-recognition.services'; | import { FacialRecognitionService } from './facial-recognition.services'; | ||||||
|  |  | ||||||
| @@ -94,6 +96,7 @@ const faceSearch = { | |||||||
| describe(FacialRecognitionService.name, () => { | describe(FacialRecognitionService.name, () => { | ||||||
|   let sut: FacialRecognitionService; |   let sut: FacialRecognitionService; | ||||||
|   let assetMock: jest.Mocked<IAssetRepository>; |   let assetMock: jest.Mocked<IAssetRepository>; | ||||||
|  |   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||||
|   let faceMock: jest.Mocked<IFaceRepository>; |   let faceMock: jest.Mocked<IFaceRepository>; | ||||||
|   let jobMock: jest.Mocked<IJobRepository>; |   let jobMock: jest.Mocked<IJobRepository>; | ||||||
|   let machineLearningMock: jest.Mocked<IMachineLearningRepository>; |   let machineLearningMock: jest.Mocked<IMachineLearningRepository>; | ||||||
| @@ -104,6 +107,7 @@ describe(FacialRecognitionService.name, () => { | |||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     assetMock = newAssetRepositoryMock(); |     assetMock = newAssetRepositoryMock(); | ||||||
|  |     configMock = newSystemConfigRepositoryMock(); | ||||||
|     faceMock = newFaceRepositoryMock(); |     faceMock = newFaceRepositoryMock(); | ||||||
|     jobMock = newJobRepositoryMock(); |     jobMock = newJobRepositoryMock(); | ||||||
|     machineLearningMock = newMachineLearningRepositoryMock(); |     machineLearningMock = newMachineLearningRepositoryMock(); | ||||||
| @@ -116,6 +120,7 @@ describe(FacialRecognitionService.name, () => { | |||||||
|  |  | ||||||
|     sut = new FacialRecognitionService( |     sut = new FacialRecognitionService( | ||||||
|       assetMock, |       assetMock, | ||||||
|  |       configMock, | ||||||
|       faceMock, |       faceMock, | ||||||
|       jobMock, |       jobMock, | ||||||
|       machineLearningMock, |       machineLearningMock, | ||||||
| @@ -174,7 +179,7 @@ describe(FacialRecognitionService.name, () => { | |||||||
|       machineLearningMock.detectFaces.mockResolvedValue([]); |       machineLearningMock.detectFaces.mockResolvedValue([]); | ||||||
|       assetMock.getByIds.mockResolvedValue([assetStub.image]); |       assetMock.getByIds.mockResolvedValue([assetStub.image]); | ||||||
|       await sut.handleRecognizeFaces({ id: assetStub.image.id }); |       await sut.handleRecognizeFaces({ id: assetStub.image.id }); | ||||||
|       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith({ |       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith('http://immich-machine-learning:3003', { | ||||||
|         imagePath: assetStub.image.resizePath, |         imagePath: assetStub.image.resizePath, | ||||||
|       }); |       }); | ||||||
|       expect(faceMock.create).not.toHaveBeenCalled(); |       expect(faceMock.create).not.toHaveBeenCalled(); | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import { Inject, Logger } from '@nestjs/common'; | import { Inject, Logger } from '@nestjs/common'; | ||||||
| import { join } from 'path'; | import { join } from 'path'; | ||||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | import { IAssetRepository, WithoutProperty } from '../asset'; | ||||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; |  | ||||||
| import { usePagination } from '../domain.util'; | import { usePagination } from '../domain.util'; | ||||||
| import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||||
| import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media'; | import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media'; | ||||||
| @@ -9,14 +8,17 @@ import { IPersonRepository } from '../person/person.repository'; | |||||||
| import { ISearchRepository } from '../search/search.repository'; | import { ISearchRepository } from '../search/search.repository'; | ||||||
| import { IMachineLearningRepository } from '../smart-info'; | import { IMachineLearningRepository } from '../smart-info'; | ||||||
| import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||||
|  | import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||||
| import { AssetFaceId, IFaceRepository } from './face.repository'; | import { AssetFaceId, IFaceRepository } from './face.repository'; | ||||||
|  |  | ||||||
| export class FacialRecognitionService { | export class FacialRecognitionService { | ||||||
|   private logger = new Logger(FacialRecognitionService.name); |   private logger = new Logger(FacialRecognitionService.name); | ||||||
|   private storageCore = new StorageCore(); |   private storageCore = new StorageCore(); | ||||||
|  |   private configCore: SystemConfigCore; | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, |     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||||
|  |     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||||
|     @Inject(IFaceRepository) private faceRepository: IFaceRepository, |     @Inject(IFaceRepository) private faceRepository: IFaceRepository, | ||||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, |     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, |     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||||
| @@ -24,9 +26,16 @@ export class FacialRecognitionService { | |||||||
|     @Inject(IPersonRepository) private personRepository: IPersonRepository, |     @Inject(IPersonRepository) private personRepository: IPersonRepository, | ||||||
|     @Inject(ISearchRepository) private searchRepository: ISearchRepository, |     @Inject(ISearchRepository) private searchRepository: ISearchRepository, | ||||||
|     @Inject(IStorageRepository) private storageRepository: IStorageRepository, |     @Inject(IStorageRepository) private storageRepository: IStorageRepository, | ||||||
|   ) {} |   ) { | ||||||
|  |     this.configCore = new SystemConfigCore(configRepository); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async handleQueueRecognizeFaces({ force }: IBaseJob) { |   async handleQueueRecognizeFaces({ force }: IBaseJob) { | ||||||
|  |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { |     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { | ||||||
|       return force |       return force | ||||||
|         ? this.assetRepository.getAll(pagination, { order: 'DESC' }) |         ? this.assetRepository.getAll(pagination, { order: 'DESC' }) | ||||||
| @@ -49,12 +58,17 @@ export class FacialRecognitionService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleRecognizeFaces({ id }: IEntityJob) { |   async handleRecognizeFaces({ id }: IEntityJob) { | ||||||
|  |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const [asset] = await this.assetRepository.getByIds([id]); |     const [asset] = await this.assetRepository.getByIds([id]); | ||||||
|     if (!asset || !MACHINE_LEARNING_ENABLED || !asset.resizePath) { |     if (!asset || !asset.resizePath) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const faces = await this.machineLearning.detectFaces({ imagePath: asset.resizePath }); |     const faces = await this.machineLearning.detectFaces(machineLearning.url, { imagePath: asset.resizePath }); | ||||||
|  |  | ||||||
|     this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); |     this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); | ||||||
|     this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` }))); |     this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` }))); | ||||||
| @@ -100,6 +114,11 @@ export class FacialRecognitionService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) { |   async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) { | ||||||
|  |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.facialRecognitionEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const { assetId, personId, boundingBox, imageWidth, imageHeight } = data; |     const { assetId, personId, boundingBox, imageWidth, imageHeight } = data; | ||||||
|  |  | ||||||
|     const [asset] = await this.assetRepository.getByIds([assetId]); |     const [asset] = await this.assetRepository.getByIds([assetId]); | ||||||
|   | |||||||
| @@ -2,8 +2,7 @@ import { AssetType } from '@app/infra/entities'; | |||||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { IAssetRepository, mapAsset } from '../asset'; | import { IAssetRepository, mapAsset } from '../asset'; | ||||||
| import { CommunicationEvent, ICommunicationRepository } from '../communication'; | import { CommunicationEvent, ICommunicationRepository } from '../communication'; | ||||||
| import { assertMachineLearningEnabled } from '../domain.constant'; | import { FeatureFlag, ISystemConfigRepository } from '../system-config'; | ||||||
| import { ISystemConfigRepository } from '../system-config'; |  | ||||||
| import { SystemConfigCore } from '../system-config/system-config.core'; | import { SystemConfigCore } from '../system-config/system-config.core'; | ||||||
| import { JobCommand, JobName, QueueName } from './job.constants'; | import { JobCommand, JobName, QueueName } from './job.constants'; | ||||||
| import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto'; | import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto'; | ||||||
| @@ -78,23 +77,25 @@ export class JobService { | |||||||
|         return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); |         return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); | ||||||
|  |  | ||||||
|       case QueueName.OBJECT_TAGGING: |       case QueueName.OBJECT_TAGGING: | ||||||
|         assertMachineLearningEnabled(); |         await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE); | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } }); | ||||||
|  |  | ||||||
|       case QueueName.CLIP_ENCODING: |       case QueueName.CLIP_ENCODING: | ||||||
|         assertMachineLearningEnabled(); |         await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE); | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); | ||||||
|  |  | ||||||
|       case QueueName.METADATA_EXTRACTION: |       case QueueName.METADATA_EXTRACTION: | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); | ||||||
|  |  | ||||||
|       case QueueName.SIDECAR: |       case QueueName.SIDECAR: | ||||||
|  |         await this.configCore.requireFeature(FeatureFlag.SIDECAR); | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); | ||||||
|  |  | ||||||
|       case QueueName.THUMBNAIL_GENERATION: |       case QueueName.THUMBNAIL_GENERATION: | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); | ||||||
|  |  | ||||||
|       case QueueName.RECOGNIZE_FACES: |       case QueueName.RECOGNIZE_FACES: | ||||||
|  |         await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION); | ||||||
|         return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } }); |         return this.jobRepository.queue({ name: JobName.QUEUE_RECOGNIZE_FACES, data: { force } }); | ||||||
|  |  | ||||||
|       default: |       default: | ||||||
|   | |||||||
| @@ -1,3 +1,2 @@ | |||||||
| export * from './search-config-response.dto'; |  | ||||||
| export * from './search-explore.response.dto'; | export * from './search-explore.response.dto'; | ||||||
| export * from './search-response.dto'; | export * from './search-response.dto'; | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| export class SearchConfigResponseDto { |  | ||||||
|   enabled!: boolean; |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| import { BadRequestException } from '@nestjs/common'; |  | ||||||
| import { ConfigService } from '@nestjs/config'; |  | ||||||
| import { | import { | ||||||
|   albumStub, |   albumStub, | ||||||
|   assetStub, |   assetStub, | ||||||
| @@ -12,12 +10,14 @@ import { | |||||||
|   newJobRepositoryMock, |   newJobRepositoryMock, | ||||||
|   newMachineLearningRepositoryMock, |   newMachineLearningRepositoryMock, | ||||||
|   newSearchRepositoryMock, |   newSearchRepositoryMock, | ||||||
|  |   newSystemConfigRepositoryMock, | ||||||
|   searchStub, |   searchStub, | ||||||
| } from '@test'; | } from '@test'; | ||||||
| import { plainToInstance } from 'class-transformer'; | import { plainToInstance } from 'class-transformer'; | ||||||
| import { IAlbumRepository } from '../album/album.repository'; | import { IAlbumRepository } from '../album/album.repository'; | ||||||
| import { IAssetRepository } from '../asset/asset.repository'; | import { IAssetRepository } from '../asset/asset.repository'; | ||||||
| import { IFaceRepository } from '../facial-recognition'; | import { IFaceRepository } from '../facial-recognition'; | ||||||
|  | import { ISystemConfigRepository } from '../index'; | ||||||
| import { JobName } from '../job'; | import { JobName } from '../job'; | ||||||
| import { IJobRepository } from '../job/job.repository'; | import { IJobRepository } from '../job/job.repository'; | ||||||
| import { IMachineLearningRepository } from '../smart-info'; | import { IMachineLearningRepository } from '../smart-info'; | ||||||
| @@ -31,29 +31,26 @@ describe(SearchService.name, () => { | |||||||
|   let sut: SearchService; |   let sut: SearchService; | ||||||
|   let albumMock: jest.Mocked<IAlbumRepository>; |   let albumMock: jest.Mocked<IAlbumRepository>; | ||||||
|   let assetMock: jest.Mocked<IAssetRepository>; |   let assetMock: jest.Mocked<IAssetRepository>; | ||||||
|  |   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||||
|   let faceMock: jest.Mocked<IFaceRepository>; |   let faceMock: jest.Mocked<IFaceRepository>; | ||||||
|   let jobMock: jest.Mocked<IJobRepository>; |   let jobMock: jest.Mocked<IJobRepository>; | ||||||
|   let machineMock: jest.Mocked<IMachineLearningRepository>; |   let machineMock: jest.Mocked<IMachineLearningRepository>; | ||||||
|   let searchMock: jest.Mocked<ISearchRepository>; |   let searchMock: jest.Mocked<ISearchRepository>; | ||||||
|   let configMock: jest.Mocked<ConfigService>; |  | ||||||
|  |  | ||||||
|   const makeSut = (value?: string) => { |   beforeEach(async () => { | ||||||
|     if (value) { |  | ||||||
|       configMock.get.mockReturnValue(value); |  | ||||||
|     } |  | ||||||
|     return new SearchService(albumMock, assetMock, faceMock, jobMock, machineMock, searchMock, configMock); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   beforeEach(() => { |  | ||||||
|     albumMock = newAlbumRepositoryMock(); |     albumMock = newAlbumRepositoryMock(); | ||||||
|     assetMock = newAssetRepositoryMock(); |     assetMock = newAssetRepositoryMock(); | ||||||
|  |     configMock = newSystemConfigRepositoryMock(); | ||||||
|     faceMock = newFaceRepositoryMock(); |     faceMock = newFaceRepositoryMock(); | ||||||
|     jobMock = newJobRepositoryMock(); |     jobMock = newJobRepositoryMock(); | ||||||
|     machineMock = newMachineLearningRepositoryMock(); |     machineMock = newMachineLearningRepositoryMock(); | ||||||
|     searchMock = newSearchRepositoryMock(); |     searchMock = newSearchRepositoryMock(); | ||||||
|     configMock = { get: jest.fn() } as unknown as jest.Mocked<ConfigService>; |  | ||||||
|  |  | ||||||
|     sut = makeSut(); |     sut = new SearchService(albumMock, assetMock, configMock, faceMock, jobMock, machineMock, searchMock); | ||||||
|  |  | ||||||
|  |     searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false }); | ||||||
|  |  | ||||||
|  |     await sut.init(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
| @@ -86,45 +83,18 @@ describe(SearchService.name, () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('isEnabled', () => { |  | ||||||
|     it('should be enabled by default', () => { |  | ||||||
|       expect(sut.isEnabled()).toBe(true); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should be disabled via an env variable', () => { |  | ||||||
|       const sut = makeSut('false'); |  | ||||||
|  |  | ||||||
|       expect(sut.isEnabled()).toBe(false); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   describe('getConfig', () => { |  | ||||||
|     it('should return the config', () => { |  | ||||||
|       expect(sut.getConfig()).toEqual({ enabled: true }); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should return the config when search is disabled', () => { |  | ||||||
|       const sut = makeSut('false'); |  | ||||||
|  |  | ||||||
|       expect(sut.getConfig()).toEqual({ enabled: false }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   describe(`init`, () => { |   describe(`init`, () => { | ||||||
|     it('should skip when search is disabled', async () => { |     // it('should skip when search is disabled', async () => { | ||||||
|       const sut = makeSut('false'); |     //   await sut.init(); | ||||||
|  |  | ||||||
|       await sut.init(); |     //   expect(searchMock.setup).not.toHaveBeenCalled(); | ||||||
|  |     //   expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled(); | ||||||
|  |     //   expect(jobMock.queue).not.toHaveBeenCalled(); | ||||||
|  |  | ||||||
|       expect(searchMock.setup).not.toHaveBeenCalled(); |     //   sut.teardown(); | ||||||
|       expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled(); |     // }); | ||||||
|       expect(jobMock.queue).not.toHaveBeenCalled(); |  | ||||||
|  |  | ||||||
|       sut.teardown(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     it('should skip schema migration if not needed', async () => { |     it('should skip schema migration if not needed', async () => { | ||||||
|       searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false, faces: false }); |  | ||||||
|       await sut.init(); |       await sut.init(); | ||||||
|  |  | ||||||
|       expect(searchMock.setup).toHaveBeenCalled(); |       expect(searchMock.setup).toHaveBeenCalled(); | ||||||
| @@ -145,14 +115,14 @@ describe(SearchService.name, () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('search', () => { |   describe('search', () => { | ||||||
|     it('should throw an error is search is disabled', async () => { |     // it('should throw an error is search is disabled', async () => { | ||||||
|       const sut = makeSut('false'); |     //   sut['enabled'] = false; | ||||||
|  |  | ||||||
|       await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException); |     //   await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException); | ||||||
|  |  | ||||||
|       expect(searchMock.searchAlbums).not.toHaveBeenCalled(); |     //   expect(searchMock.searchAlbums).not.toHaveBeenCalled(); | ||||||
|       expect(searchMock.searchAssets).not.toHaveBeenCalled(); |     //   expect(searchMock.searchAssets).not.toHaveBeenCalled(); | ||||||
|     }); |     // }); | ||||||
|  |  | ||||||
|     it('should search assets and albums', async () => { |     it('should search assets and albums', async () => { | ||||||
|       searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults); |       searchMock.searchAssets.mockResolvedValue(searchStub.emptyResults); | ||||||
| @@ -205,7 +175,7 @@ describe(SearchService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should skip if search is disabled', async () => { |     it('should skip if search is disabled', async () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|  |  | ||||||
|       await sut.handleIndexAssets(); |       await sut.handleIndexAssets(); | ||||||
|  |  | ||||||
| @@ -216,7 +186,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleIndexAsset', () => { |   describe('handleIndexAsset', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleIndexAsset({ ids: [assetStub.image.id] }); |       sut.handleIndexAsset({ ids: [assetStub.image.id] }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -227,7 +197,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleIndexAlbums', () => { |   describe('handleIndexAlbums', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleIndexAlbums(); |       sut.handleIndexAlbums(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -242,7 +212,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleIndexAlbum', () => { |   describe('handleIndexAlbum', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleIndexAlbum({ ids: [albumStub.empty.id] }); |       sut.handleIndexAlbum({ ids: [albumStub.empty.id] }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -253,7 +223,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleRemoveAlbum', () => { |   describe('handleRemoveAlbum', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleRemoveAlbum({ ids: ['album1'] }); |       sut.handleRemoveAlbum({ ids: ['album1'] }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -264,7 +234,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleRemoveAsset', () => { |   describe('handleRemoveAsset', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleRemoveAsset({ ids: ['asset1'] }); |       sut.handleRemoveAsset({ ids: ['asset1'] }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -305,7 +275,7 @@ describe(SearchService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should skip if search is disabled', async () => { |     it('should skip if search is disabled', async () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|  |  | ||||||
|       await sut.handleIndexFaces(); |       await sut.handleIndexFaces(); | ||||||
|  |  | ||||||
| @@ -315,7 +285,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleIndexAsset', () => { |   describe('handleIndexAsset', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' }); |       sut.handleIndexFace({ assetId: 'asset-1', personId: 'person-1' }); | ||||||
|  |  | ||||||
|       expect(searchMock.importFaces).not.toHaveBeenCalled(); |       expect(searchMock.importFaces).not.toHaveBeenCalled(); | ||||||
| @@ -333,7 +303,7 @@ describe(SearchService.name, () => { | |||||||
|  |  | ||||||
|   describe('handleRemoveFace', () => { |   describe('handleRemoveFace', () => { | ||||||
|     it('should skip if search is disabled', () => { |     it('should skip if search is disabled', () => { | ||||||
|       const sut = makeSut('false'); |       sut['enabled'] = false; | ||||||
|       sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' }); |       sut.handleRemoveFace({ assetId: 'asset-1', personId: 'person-1' }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,17 @@ | |||||||
| import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities'; | import { AlbumEntity, AssetEntity, AssetFaceEntity } from '@app/infra/entities'; | ||||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||||
| import { ConfigService } from '@nestjs/config'; |  | ||||||
| import { mapAlbumWithAssets } from '../album'; | import { mapAlbumWithAssets } from '../album'; | ||||||
| import { IAlbumRepository } from '../album/album.repository'; | import { IAlbumRepository } from '../album/album.repository'; | ||||||
| import { AssetResponseDto, mapAsset } from '../asset'; | import { AssetResponseDto, mapAsset } from '../asset'; | ||||||
| import { IAssetRepository } from '../asset/asset.repository'; | import { IAssetRepository } from '../asset/asset.repository'; | ||||||
| import { AuthUserDto } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; |  | ||||||
| import { usePagination } from '../domain.util'; | import { usePagination } from '../domain.util'; | ||||||
| import { AssetFaceId, IFaceRepository } from '../facial-recognition'; | import { AssetFaceId, IFaceRepository } from '../facial-recognition'; | ||||||
| import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | import { IAssetFaceJob, IBulkEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||||
| import { IMachineLearningRepository } from '../smart-info'; | import { IMachineLearningRepository } from '../smart-info'; | ||||||
|  | import { FeatureFlag, ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||||
| import { SearchDto } from './dto'; | import { SearchDto } from './dto'; | ||||||
| import { SearchConfigResponseDto, SearchResponseDto } from './response-dto'; | import { SearchResponseDto } from './response-dto'; | ||||||
| import { | import { | ||||||
|   ISearchRepository, |   ISearchRepository, | ||||||
|   OwnedFaceEntity, |   OwnedFaceEntity, | ||||||
| @@ -30,8 +29,9 @@ interface SyncQueue { | |||||||
| @Injectable() | @Injectable() | ||||||
| export class SearchService { | export class SearchService { | ||||||
|   private logger = new Logger(SearchService.name); |   private logger = new Logger(SearchService.name); | ||||||
|   private enabled: boolean; |   private enabled = false; | ||||||
|   private timer: NodeJS.Timer | null = null; |   private timer: NodeJS.Timer | null = null; | ||||||
|  |   private configCore: SystemConfigCore; | ||||||
|  |  | ||||||
|   private albumQueue: SyncQueue = { |   private albumQueue: SyncQueue = { | ||||||
|     upsert: new Set(), |     upsert: new Set(), | ||||||
| @@ -51,16 +51,13 @@ export class SearchService { | |||||||
|   constructor( |   constructor( | ||||||
|     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, |     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, | ||||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, |     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||||
|  |     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||||
|     @Inject(IFaceRepository) private faceRepository: IFaceRepository, |     @Inject(IFaceRepository) private faceRepository: IFaceRepository, | ||||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, |     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, |     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||||
|     @Inject(ISearchRepository) private searchRepository: ISearchRepository, |     @Inject(ISearchRepository) private searchRepository: ISearchRepository, | ||||||
|     configService: ConfigService, |  | ||||||
|   ) { |   ) { | ||||||
|     this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false'; |     this.configCore = new SystemConfigCore(configRepository); | ||||||
|     if (this.enabled) { |  | ||||||
|       this.timer = setInterval(() => this.flush(), 5_000); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   teardown() { |   teardown() { | ||||||
| @@ -70,17 +67,8 @@ export class SearchService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isEnabled() { |  | ||||||
|     return this.enabled; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getConfig(): SearchConfigResponseDto { |  | ||||||
|     return { |  | ||||||
|       enabled: this.enabled, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async init() { |   async init() { | ||||||
|  |     this.enabled = await this.configCore.hasFeature(FeatureFlag.SEARCH); | ||||||
|     if (!this.enabled) { |     if (!this.enabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -101,10 +89,13 @@ export class SearchService { | |||||||
|       this.logger.debug('Queueing job to re-index all faces'); |       this.logger.debug('Queueing job to re-index all faces'); | ||||||
|       await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES }); |       await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     this.timer = setInterval(() => this.flush(), 5_000); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> { |   async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> { | ||||||
|     this.assertEnabled(); |     await this.configCore.requireFeature(FeatureFlag.SEARCH); | ||||||
|  |  | ||||||
|     const results = await this.searchRepository.explore(authUser.id); |     const results = await this.searchRepository.explore(authUser.id); | ||||||
|     const lookup = await this.getLookupMap( |     const lookup = await this.getLookupMap( | ||||||
|       results.reduce( |       results.reduce( | ||||||
| @@ -126,16 +117,18 @@ export class SearchService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> { |   async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> { | ||||||
|     this.assertEnabled(); |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     await this.configCore.requireFeature(FeatureFlag.SEARCH); | ||||||
|  |  | ||||||
|     const query = dto.q || dto.query || '*'; |     const query = dto.q || dto.query || '*'; | ||||||
|     const strategy = dto.clip && MACHINE_LEARNING_ENABLED ? SearchStrategy.CLIP : SearchStrategy.TEXT; |     const hasClip = machineLearning.enabled && machineLearning.clipEncodeEnabled; | ||||||
|  |     const strategy = dto.clip && hasClip ? SearchStrategy.CLIP : SearchStrategy.TEXT; | ||||||
|     const filters = { userId: authUser.id, ...dto }; |     const filters = { userId: authUser.id, ...dto }; | ||||||
|  |  | ||||||
|     let assets: SearchResult<AssetEntity>; |     let assets: SearchResult<AssetEntity>; | ||||||
|     switch (strategy) { |     switch (strategy) { | ||||||
|       case SearchStrategy.CLIP: |       case SearchStrategy.CLIP: | ||||||
|         const clip = await this.machineLearning.encodeText(query); |         const clip = await this.machineLearning.encodeText(machineLearning.url, query); | ||||||
|         assets = await this.searchRepository.vectorSearch(clip, filters); |         assets = await this.searchRepository.vectorSearch(clip, filters); | ||||||
|         break; |         break; | ||||||
|       case SearchStrategy.TEXT: |       case SearchStrategy.TEXT: | ||||||
| @@ -333,12 +326,6 @@ export class SearchService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private assertEnabled() { |  | ||||||
|     if (!this.enabled) { |  | ||||||
|       throw new BadRequestException('Search is disabled'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> { |   private async idsToAlbums(ids: string[]): Promise<AlbumEntity[]> { | ||||||
|     const entities = await this.albumRepository.getByIds(ids); |     const entities = await this.albumRepository.getByIds(ids); | ||||||
|     return this.patchAlbums(entities); |     return this.patchAlbums(entities); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { IServerVersion } from '@app/domain'; | import { FeatureFlags, IServerVersion } from '@app/domain'; | ||||||
| import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; | import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; | ||||||
|  |  | ||||||
| export class ServerPingResponse { | export class ServerPingResponse { | ||||||
| @@ -79,10 +79,14 @@ export class ServerMediaTypesResponseDto { | |||||||
|   sidecar!: string[]; |   sidecar!: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class ServerFeaturesDto { | export class ServerFeaturesDto implements FeatureFlags { | ||||||
|   machineLearning!: boolean; |   clipEncode!: boolean; | ||||||
|  |   facialRecognition!: boolean; | ||||||
|  |   sidecar!: boolean; | ||||||
|   search!: boolean; |   search!: boolean; | ||||||
|  |   tagImage!: boolean; | ||||||
|  |  | ||||||
|  |   // TODO: use these instead of `POST oauth/config` | ||||||
|   oauth!: boolean; |   oauth!: boolean; | ||||||
|   oauthAutoLaunch!: boolean; |   oauthAutoLaunch!: boolean; | ||||||
|   passwordLogin!: boolean; |   passwordLogin!: boolean; | ||||||
|   | |||||||
| @@ -147,11 +147,14 @@ describe(ServerInfoService.name, () => { | |||||||
|     describe('getFeatures', () => { |     describe('getFeatures', () => { | ||||||
|       it('should respond the server features', async () => { |       it('should respond the server features', async () => { | ||||||
|         await expect(sut.getFeatures()).resolves.toEqual({ |         await expect(sut.getFeatures()).resolves.toEqual({ | ||||||
|           machineLearning: true, |           clipEncode: true, | ||||||
|  |           facialRecognition: true, | ||||||
|           oauth: false, |           oauth: false, | ||||||
|           oauthAutoLaunch: false, |           oauthAutoLaunch: false, | ||||||
|           passwordLogin: true, |           passwordLogin: true, | ||||||
|           search: true, |           search: true, | ||||||
|  |           sidecar: true, | ||||||
|  |           tagImage: true, | ||||||
|         }); |         }); | ||||||
|         expect(configMock.load).toHaveBeenCalled(); |         expect(configMock.load).toHaveBeenCalled(); | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -1,9 +1,8 @@ | |||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { MACHINE_LEARNING_ENABLED, mimeTypes, SEARCH_ENABLED, serverVersion } from '../domain.constant'; | import { mimeTypes, serverVersion } from '../domain.constant'; | ||||||
| import { asHumanReadable } from '../domain.util'; | import { asHumanReadable } from '../domain.util'; | ||||||
| import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||||
| import { ISystemConfigRepository } from '../system-config'; | import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||||
| import { SystemConfigCore } from '../system-config/system-config.core'; |  | ||||||
| import { IUserRepository, UserStatsQueryResponse } from '../user'; | import { IUserRepository, UserStatsQueryResponse } from '../user'; | ||||||
| import { | import { | ||||||
|   ServerFeaturesDto, |   ServerFeaturesDto, | ||||||
| @@ -52,18 +51,8 @@ export class ServerInfoService { | |||||||
|     return serverVersion; |     return serverVersion; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getFeatures(): Promise<ServerFeaturesDto> { |   getFeatures(): Promise<ServerFeaturesDto> { | ||||||
|     const config = await this.configCore.getConfig(); |     return this.configCore.getFeatures(); | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       machineLearning: MACHINE_LEARNING_ENABLED, |  | ||||||
|       search: SEARCH_ENABLED, |  | ||||||
|  |  | ||||||
|       // TODO: use these instead of `POST oauth/config` |  | ||||||
|       oauth: config.oauth.enabled, |  | ||||||
|       oauthAutoLaunch: config.oauth.autoLaunch, |  | ||||||
|       passwordLogin: config.passwordLogin.enabled, |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getStats(): Promise<ServerStatsResponseDto> { |   async getStats(): Promise<ServerStatsResponseDto> { | ||||||
|   | |||||||
| @@ -20,8 +20,8 @@ export interface DetectFaceResult { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface IMachineLearningRepository { | export interface IMachineLearningRepository { | ||||||
|   classifyImage(input: MachineLearningInput): Promise<string[]>; |   classifyImage(url: string, input: MachineLearningInput): Promise<string[]>; | ||||||
|   encodeImage(input: MachineLearningInput): Promise<number[]>; |   encodeImage(url: string, input: MachineLearningInput): Promise<number[]>; | ||||||
|   encodeText(input: string): Promise<number[]>; |   encodeText(url: string, input: string): Promise<number[]>; | ||||||
|   detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]>; |   detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]>; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,9 +5,11 @@ import { | |||||||
|   newJobRepositoryMock, |   newJobRepositoryMock, | ||||||
|   newMachineLearningRepositoryMock, |   newMachineLearningRepositoryMock, | ||||||
|   newSmartInfoRepositoryMock, |   newSmartInfoRepositoryMock, | ||||||
|  |   newSystemConfigRepositoryMock, | ||||||
| } from '@test'; | } from '@test'; | ||||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | import { IAssetRepository, WithoutProperty } from '../asset'; | ||||||
| import { IJobRepository, JobName } from '../job'; | import { IJobRepository, JobName } from '../job'; | ||||||
|  | import { ISystemConfigRepository } from '../system-config'; | ||||||
| import { IMachineLearningRepository } from './machine-learning.interface'; | import { IMachineLearningRepository } from './machine-learning.interface'; | ||||||
| import { ISmartInfoRepository } from './smart-info.repository'; | import { ISmartInfoRepository } from './smart-info.repository'; | ||||||
| import { SmartInfoService } from './smart-info.service'; | import { SmartInfoService } from './smart-info.service'; | ||||||
| @@ -20,16 +22,18 @@ const asset = { | |||||||
| describe(SmartInfoService.name, () => { | describe(SmartInfoService.name, () => { | ||||||
|   let sut: SmartInfoService; |   let sut: SmartInfoService; | ||||||
|   let assetMock: jest.Mocked<IAssetRepository>; |   let assetMock: jest.Mocked<IAssetRepository>; | ||||||
|  |   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||||
|   let jobMock: jest.Mocked<IJobRepository>; |   let jobMock: jest.Mocked<IJobRepository>; | ||||||
|   let smartMock: jest.Mocked<ISmartInfoRepository>; |   let smartMock: jest.Mocked<ISmartInfoRepository>; | ||||||
|   let machineMock: jest.Mocked<IMachineLearningRepository>; |   let machineMock: jest.Mocked<IMachineLearningRepository>; | ||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     assetMock = newAssetRepositoryMock(); |     assetMock = newAssetRepositoryMock(); | ||||||
|  |     configMock = newSystemConfigRepositoryMock(); | ||||||
|     smartMock = newSmartInfoRepositoryMock(); |     smartMock = newSmartInfoRepositoryMock(); | ||||||
|     jobMock = newJobRepositoryMock(); |     jobMock = newJobRepositoryMock(); | ||||||
|     machineMock = newMachineLearningRepositoryMock(); |     machineMock = newMachineLearningRepositoryMock(); | ||||||
|     sut = new SmartInfoService(assetMock, jobMock, smartMock, machineMock); |     sut = new SmartInfoService(assetMock, configMock, jobMock, smartMock, machineMock); | ||||||
|  |  | ||||||
|     assetMock.getByIds.mockResolvedValue([asset]); |     assetMock.getByIds.mockResolvedValue([asset]); | ||||||
|   }); |   }); | ||||||
| @@ -80,7 +84,9 @@ describe(SmartInfoService.name, () => { | |||||||
|  |  | ||||||
|       await sut.handleClassifyImage({ id: asset.id }); |       await sut.handleClassifyImage({ id: asset.id }); | ||||||
|  |  | ||||||
|       expect(machineMock.classifyImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' }); |       expect(machineMock.classifyImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', { | ||||||
|  |         imagePath: 'path/to/resize.ext', | ||||||
|  |       }); | ||||||
|       expect(smartMock.upsert).toHaveBeenCalledWith({ |       expect(smartMock.upsert).toHaveBeenCalledWith({ | ||||||
|         assetId: 'asset-1', |         assetId: 'asset-1', | ||||||
|         tags: ['tag1', 'tag2', 'tag3'], |         tags: ['tag1', 'tag2', 'tag3'], | ||||||
| @@ -139,7 +145,9 @@ describe(SmartInfoService.name, () => { | |||||||
|  |  | ||||||
|       await sut.handleEncodeClip({ id: asset.id }); |       await sut.handleEncodeClip({ id: asset.id }); | ||||||
|  |  | ||||||
|       expect(machineMock.encodeImage).toHaveBeenCalledWith({ imagePath: 'path/to/resize.ext' }); |       expect(machineMock.encodeImage).toHaveBeenCalledWith('http://immich-machine-learning:3003', { | ||||||
|  |         imagePath: 'path/to/resize.ext', | ||||||
|  |       }); | ||||||
|       expect(smartMock.upsert).toHaveBeenCalledWith({ |       expect(smartMock.upsert).toHaveBeenCalledWith({ | ||||||
|         assetId: 'asset-1', |         assetId: 'asset-1', | ||||||
|         clipEmbedding: [0.01, 0.02, 0.03], |         clipEmbedding: [0.01, 0.02, 0.03], | ||||||
|   | |||||||
| @@ -1,23 +1,31 @@ | |||||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { IAssetRepository, WithoutProperty } from '../asset'; | import { IAssetRepository, WithoutProperty } from '../asset'; | ||||||
| import { MACHINE_LEARNING_ENABLED } from '../domain.constant'; |  | ||||||
| import { usePagination } from '../domain.util'; | import { usePagination } from '../domain.util'; | ||||||
| import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SIZE } from '../job'; | ||||||
|  | import { ISystemConfigRepository, SystemConfigCore } from '../system-config'; | ||||||
| import { IMachineLearningRepository } from './machine-learning.interface'; | import { IMachineLearningRepository } from './machine-learning.interface'; | ||||||
| import { ISmartInfoRepository } from './smart-info.repository'; | import { ISmartInfoRepository } from './smart-info.repository'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class SmartInfoService { | export class SmartInfoService { | ||||||
|   private logger = new Logger(SmartInfoService.name); |   private configCore: SystemConfigCore; | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(IAssetRepository) private assetRepository: IAssetRepository, |     @Inject(IAssetRepository) private assetRepository: IAssetRepository, | ||||||
|  |     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, | ||||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, |     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||||
|     @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, |     @Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, | ||||||
|     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, |     @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, | ||||||
|   ) {} |   ) { | ||||||
|  |     this.configCore = new SystemConfigCore(configRepository); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async handleQueueObjectTagging({ force }: IBaseJob) { |   async handleQueueObjectTagging({ force }: IBaseJob) { | ||||||
|  |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.tagImageEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { |     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { | ||||||
|       return force |       return force | ||||||
|         ? this.assetRepository.getAll(pagination) |         ? this.assetRepository.getAll(pagination) | ||||||
| @@ -34,19 +42,28 @@ export class SmartInfoService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleClassifyImage({ id }: IEntityJob) { |   async handleClassifyImage({ id }: IEntityJob) { | ||||||
|     const [asset] = await this.assetRepository.getByIds([id]); |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.tagImageEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) { |     const [asset] = await this.assetRepository.getByIds([id]); | ||||||
|  |     if (!asset.resizePath) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const tags = await this.machineLearning.classifyImage({ imagePath: asset.resizePath }); |     const tags = await this.machineLearning.classifyImage(machineLearning.url, { imagePath: asset.resizePath }); | ||||||
|     await this.repository.upsert({ assetId: asset.id, tags }); |     await this.repository.upsert({ assetId: asset.id, tags }); | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleQueueEncodeClip({ force }: IBaseJob) { |   async handleQueueEncodeClip({ force }: IBaseJob) { | ||||||
|  |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { |     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { | ||||||
|       return force |       return force | ||||||
|         ? this.assetRepository.getAll(pagination) |         ? this.assetRepository.getAll(pagination) | ||||||
| @@ -63,13 +80,17 @@ export class SmartInfoService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async handleEncodeClip({ id }: IEntityJob) { |   async handleEncodeClip({ id }: IEntityJob) { | ||||||
|     const [asset] = await this.assetRepository.getByIds([id]); |     const { machineLearning } = await this.configCore.getConfig(); | ||||||
|  |     if (!machineLearning.enabled || !machineLearning.clipEncodeEnabled) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) { |     const [asset] = await this.assetRepository.getByIds([id]); | ||||||
|  |     if (!asset.resizePath) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const clipEmbedding = await this.machineLearning.encodeImage({ imagePath: asset.resizePath }); |     const clipEmbedding = await this.machineLearning.encodeImage(machineLearning.url, { imagePath: asset.resizePath }); | ||||||
|     await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding }); |     await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding }); | ||||||
|  |  | ||||||
|     return true; |     return true; | ||||||
|   | |||||||
| @@ -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 { IsObject, ValidateNested } from 'class-validator'; | ||||||
| import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; | import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; | ||||||
| import { SystemConfigJobDto } from './system-config-job.dto'; | import { SystemConfigJobDto } from './system-config-job.dto'; | ||||||
|  | import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto'; | ||||||
| import { SystemConfigOAuthDto } from './system-config-oauth.dto'; | import { SystemConfigOAuthDto } from './system-config-oauth.dto'; | ||||||
| import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; | import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; | ||||||
| import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; | import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; | ||||||
|  |  | ||||||
| export class SystemConfigDto { | export class SystemConfigDto implements SystemConfig { | ||||||
|   @Type(() => SystemConfigFFmpegDto) |   @Type(() => SystemConfigFFmpegDto) | ||||||
|   @ValidateNested() |   @ValidateNested() | ||||||
|   @IsObject() |   @IsObject() | ||||||
|   ffmpeg!: SystemConfigFFmpegDto; |   ffmpeg!: SystemConfigFFmpegDto; | ||||||
|  |  | ||||||
|  |   @Type(() => SystemConfigMachineLearningDto) | ||||||
|  |   @ValidateNested() | ||||||
|  |   @IsObject() | ||||||
|  |   machineLearning!: SystemConfigMachineLearningDto; | ||||||
|  |  | ||||||
|   @Type(() => SystemConfigOAuthDto) |   @Type(() => SystemConfigOAuthDto) | ||||||
|   @ValidateNested() |   @ValidateNested() | ||||||
|   @IsObject() |   @IsObject() | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| export * from './dto'; | export * from './dto'; | ||||||
| export * from './response-dto'; | export * from './response-dto'; | ||||||
| export * from './system-config.constants'; | export * from './system-config.constants'; | ||||||
|  | export * from './system-config.core'; | ||||||
| export * from './system-config.repository'; | export * from './system-config.repository'; | ||||||
| export * from './system-config.service'; | export * from './system-config.service'; | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { | |||||||
|   TranscodePolicy, |   TranscodePolicy, | ||||||
|   VideoCodec, |   VideoCodec, | ||||||
| } from '@app/infra/entities'; | } from '@app/infra/entities'; | ||||||
| import { BadRequestException, Injectable, Logger } from '@nestjs/common'; | import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common'; | ||||||
| import * as _ from 'lodash'; | import * as _ from 'lodash'; | ||||||
| import { Subject } from 'rxjs'; | import { Subject } from 'rxjs'; | ||||||
| import { DeepPartial } from 'typeorm'; | import { DeepPartial } from 'typeorm'; | ||||||
| @@ -44,6 +44,13 @@ export const defaults = Object.freeze<SystemConfig>({ | |||||||
|     [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, |     [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, | ||||||
|     [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, |     [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, | ||||||
|   }, |   }, | ||||||
|  |   machineLearning: { | ||||||
|  |     enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', | ||||||
|  |     url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003', | ||||||
|  |     facialRecognitionEnabled: true, | ||||||
|  |     tagImageEnabled: true, | ||||||
|  |     clipEncodeEnabled: true, | ||||||
|  |   }, | ||||||
|   oauth: { |   oauth: { | ||||||
|     enabled: false, |     enabled: false, | ||||||
|     issuerUrl: '', |     issuerUrl: '', | ||||||
| @@ -71,6 +78,19 @@ export const defaults = Object.freeze<SystemConfig>({ | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | export enum FeatureFlag { | ||||||
|  |   CLIP_ENCODE = 'clipEncode', | ||||||
|  |   FACIAL_RECOGNITION = 'facialRecognition', | ||||||
|  |   TAG_IMAGE = 'tagImage', | ||||||
|  |   SIDECAR = 'sidecar', | ||||||
|  |   SEARCH = 'search', | ||||||
|  |   OAUTH = 'oauth', | ||||||
|  |   OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch', | ||||||
|  |   PASSWORD_LOGIN = 'passwordLogin', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type FeatureFlags = Record<FeatureFlag, boolean>; | ||||||
|  |  | ||||||
| const singleton = new Subject<SystemConfig>(); | const singleton = new Subject<SystemConfig>(); | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| @@ -82,6 +102,53 @@ export class SystemConfigCore { | |||||||
|  |  | ||||||
|   constructor(private repository: ISystemConfigRepository) {} |   constructor(private repository: ISystemConfigRepository) {} | ||||||
|  |  | ||||||
|  |   async requireFeature(feature: FeatureFlag) { | ||||||
|  |     const hasFeature = await this.hasFeature(feature); | ||||||
|  |     if (!hasFeature) { | ||||||
|  |       switch (feature) { | ||||||
|  |         case FeatureFlag.CLIP_ENCODE: | ||||||
|  |           throw new BadRequestException('Clip encoding is not enabled'); | ||||||
|  |         case FeatureFlag.FACIAL_RECOGNITION: | ||||||
|  |           throw new BadRequestException('Facial recognition is not enabled'); | ||||||
|  |         case FeatureFlag.TAG_IMAGE: | ||||||
|  |           throw new BadRequestException('Image tagging is not enabled'); | ||||||
|  |         case FeatureFlag.SIDECAR: | ||||||
|  |           throw new BadRequestException('Sidecar is not enabled'); | ||||||
|  |         case FeatureFlag.SEARCH: | ||||||
|  |           throw new BadRequestException('Search is not enabled'); | ||||||
|  |         case FeatureFlag.OAUTH: | ||||||
|  |           throw new BadRequestException('OAuth is not enabled'); | ||||||
|  |         case FeatureFlag.PASSWORD_LOGIN: | ||||||
|  |           throw new BadRequestException('Password login is not enabled'); | ||||||
|  |         default: | ||||||
|  |           throw new ForbiddenException(`Missing required feature: ${feature}`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async hasFeature(feature: FeatureFlag) { | ||||||
|  |     const features = await this.getFeatures(); | ||||||
|  |     return features[feature] ?? false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async getFeatures(): Promise<FeatureFlags> { | ||||||
|  |     const config = await this.getConfig(); | ||||||
|  |     const mlEnabled = config.machineLearning.enabled; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       [FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clipEncodeEnabled, | ||||||
|  |       [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognitionEnabled, | ||||||
|  |       [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.tagImageEnabled, | ||||||
|  |       [FeatureFlag.SIDECAR]: true, | ||||||
|  |       [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false', | ||||||
|  |  | ||||||
|  |       // TODO: use these instead of `POST oauth/config` | ||||||
|  |       [FeatureFlag.OAUTH]: config.oauth.enabled, | ||||||
|  |       [FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch, | ||||||
|  |       [FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public getDefaults(): SystemConfig { |   public getDefaults(): SystemConfig { | ||||||
|     return defaults; |     return defaults; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -46,6 +46,13 @@ const updatedConfig = Object.freeze<SystemConfig>({ | |||||||
|     accel: TranscodeHWAccel.DISABLED, |     accel: TranscodeHWAccel.DISABLED, | ||||||
|     tonemap: ToneMapping.HABLE, |     tonemap: ToneMapping.HABLE, | ||||||
|   }, |   }, | ||||||
|  |   machineLearning: { | ||||||
|  |     enabled: true, | ||||||
|  |     url: 'http://immich-machine-learning:3003', | ||||||
|  |     facialRecognitionEnabled: true, | ||||||
|  |     tagImageEnabled: true, | ||||||
|  |     clipEncodeEnabled: true, | ||||||
|  |   }, | ||||||
|   oauth: { |   oauth: { | ||||||
|     autoLaunch: true, |     autoLaunch: true, | ||||||
|     autoRegister: true, |     autoRegister: true, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { JobService, MACHINE_LEARNING_ENABLED, SearchService, StorageService } from '@app/domain'; | import { JobService, SearchService, ServerInfoService, StorageService } from '@app/domain'; | ||||||
| import { Injectable, Logger } from '@nestjs/common'; | import { Injectable, Logger } from '@nestjs/common'; | ||||||
| import { Cron, CronExpression } from '@nestjs/schedule'; | import { Cron, CronExpression } from '@nestjs/schedule'; | ||||||
|  |  | ||||||
| @@ -10,6 +10,7 @@ export class AppService { | |||||||
|     private jobService: JobService, |     private jobService: JobService, | ||||||
|     private searchService: SearchService, |     private searchService: SearchService, | ||||||
|     private storageService: StorageService, |     private storageService: StorageService, | ||||||
|  |     private serverService: ServerInfoService, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) |   @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) | ||||||
| @@ -20,8 +21,6 @@ export class AppService { | |||||||
|   async init() { |   async init() { | ||||||
|     this.storageService.init(); |     this.storageService.init(); | ||||||
|     await this.searchService.init(); |     await this.searchService.init(); | ||||||
|  |     this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); | ||||||
|     this.logger.log(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`); |  | ||||||
|     this.logger.log(`Search is ${this.searchService.isEnabled() ? 'enabled' : 'disabled'}`); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,4 @@ | |||||||
| import { | import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain'; | ||||||
|   AuthUserDto, |  | ||||||
|   SearchConfigResponseDto, |  | ||||||
|   SearchDto, |  | ||||||
|   SearchExploreResponseDto, |  | ||||||
|   SearchResponseDto, |  | ||||||
|   SearchService, |  | ||||||
| } from '@app/domain'; |  | ||||||
| import { Controller, Get, Query } from '@nestjs/common'; | import { Controller, Get, Query } from '@nestjs/common'; | ||||||
| import { ApiTags } from '@nestjs/swagger'; | import { ApiTags } from '@nestjs/swagger'; | ||||||
| import { Authenticated, AuthUser } from '../app.guard'; | import { Authenticated, AuthUser } from '../app.guard'; | ||||||
| @@ -23,11 +16,6 @@ export class SearchController { | |||||||
|     return this.service.search(authUser, dto); |     return this.service.search(authUser, dto); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Get('config') |  | ||||||
|   getSearchConfig(): SearchConfigResponseDto { |  | ||||||
|     return this.service.getConfig(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Get('explore') |   @Get('explore') | ||||||
|   getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> { |   getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> { | ||||||
|     return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>; |     return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>; | ||||||
|   | |||||||
| @@ -37,6 +37,12 @@ export enum SystemConfigKey { | |||||||
|   JOB_SEARCH_CONCURRENCY = 'job.search.concurrency', |   JOB_SEARCH_CONCURRENCY = 'job.search.concurrency', | ||||||
|   JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency', |   JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency', | ||||||
|  |  | ||||||
|  |   MACHINE_LEARNING_ENABLED = 'machineLearning.enabled', | ||||||
|  |   MACHINE_LEARNING_URL = 'machineLearning.url', | ||||||
|  |   MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED = 'machineLearning.facialRecognitionEnabled', | ||||||
|  |   MACHINE_LEARNING_TAG_IMAGE_ENABLED = 'machineLearning.tagImageEnabled', | ||||||
|  |   MACHINE_LEARNING_CLIP_ENCODE_ENABLED = 'machineLearning.clipEncodeEnabled', | ||||||
|  |  | ||||||
|   OAUTH_ENABLED = 'oauth.enabled', |   OAUTH_ENABLED = 'oauth.enabled', | ||||||
|   OAUTH_ISSUER_URL = 'oauth.issuerUrl', |   OAUTH_ISSUER_URL = 'oauth.issuerUrl', | ||||||
|   OAUTH_CLIENT_ID = 'oauth.clientId', |   OAUTH_CLIENT_ID = 'oauth.clientId', | ||||||
| @@ -105,6 +111,13 @@ export interface SystemConfig { | |||||||
|     tonemap: ToneMapping; |     tonemap: ToneMapping; | ||||||
|   }; |   }; | ||||||
|   job: Record<QueueName, { concurrency: number }>; |   job: Record<QueueName, { concurrency: number }>; | ||||||
|  |   machineLearning: { | ||||||
|  |     enabled: boolean; | ||||||
|  |     url: string; | ||||||
|  |     clipEncodeEnabled: boolean; | ||||||
|  |     facialRecognitionEnabled: boolean; | ||||||
|  |     tagImageEnabled: boolean; | ||||||
|  |   }; | ||||||
|   oauth: { |   oauth: { | ||||||
|     enabled: boolean; |     enabled: boolean; | ||||||
|     issuerUrl: string; |     issuerUrl: string; | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput, MACHINE_LEARNING_URL } from '@app/domain'; | import { DetectFaceResult, IMachineLearningRepository, MachineLearningInput } from '@app/domain'; | ||||||
| import { Injectable } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import { createReadStream } from 'fs'; | import { createReadStream } from 'fs'; | ||||||
|  |  | ||||||
| const client = axios.create({ baseURL: MACHINE_LEARNING_URL }); | const client = axios.create(); | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class MachineLearningRepository implements IMachineLearningRepository { | export class MachineLearningRepository implements IMachineLearningRepository { | ||||||
| @@ -11,19 +11,19 @@ export class MachineLearningRepository implements IMachineLearningRepository { | |||||||
|     return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data); |     return client.post<T>(endpoint, createReadStream(input.imagePath)).then((res) => res.data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   classifyImage(input: MachineLearningInput): Promise<string[]> { |   classifyImage(url: string, input: MachineLearningInput): Promise<string[]> { | ||||||
|     return this.post<string[]>(input, '/image-classifier/tag-image'); |     return this.post<string[]>(input, `${url}/image-classifier/tag-image`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   detectFaces(input: MachineLearningInput): Promise<DetectFaceResult[]> { |   detectFaces(url: string, input: MachineLearningInput): Promise<DetectFaceResult[]> { | ||||||
|     return this.post<DetectFaceResult[]>(input, '/facial-recognition/detect-faces'); |     return this.post<DetectFaceResult[]>(input, `${url}/facial-recognition/detect-faces`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   encodeImage(input: MachineLearningInput): Promise<number[]> { |   encodeImage(url: string, input: MachineLearningInput): Promise<number[]> { | ||||||
|     return this.post<number[]>(input, '/sentence-transformer/encode-image'); |     return this.post<number[]>(input, `${url}/sentence-transformer/encode-image`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   encodeText(input: string): Promise<number[]> { |   encodeText(url: string, input: string): Promise<number[]> { | ||||||
|     return client.post<number[]>('/sentence-transformer/encode-text', { text: input }).then((res) => res.data); |     return client.post<number[]>(`${url}/sentence-transformer/encode-text`, { text: input }).then((res) => res.data); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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; |     'total': number; | ||||||
| } | } | ||||||
| /** |  | ||||||
|  *  |  | ||||||
|  * @export |  | ||||||
|  * @interface SearchConfigResponseDto |  | ||||||
|  */ |  | ||||||
| export interface SearchConfigResponseDto { |  | ||||||
|     /** |  | ||||||
|      *  |  | ||||||
|      * @type {boolean} |  | ||||||
|      * @memberof SearchConfigResponseDto |  | ||||||
|      */ |  | ||||||
|     'enabled': boolean; |  | ||||||
| } |  | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|  * @export |  * @export | ||||||
| @@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto { | |||||||
|      * @type {boolean} |      * @type {boolean} | ||||||
|      * @memberof ServerFeaturesDto |      * @memberof ServerFeaturesDto | ||||||
|      */ |      */ | ||||||
|     'machineLearning': boolean; |     'clipEncode': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'facialRecognition': boolean; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {boolean} |      * @type {boolean} | ||||||
| @@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto { | |||||||
|      * @memberof ServerFeaturesDto |      * @memberof ServerFeaturesDto | ||||||
|      */ |      */ | ||||||
|     'search': boolean; |     'search': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'sidecar': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof ServerFeaturesDto | ||||||
|  |      */ | ||||||
|  |     'tagImage': boolean; | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
| @@ -2611,6 +2616,12 @@ export interface SystemConfigDto { | |||||||
|      * @memberof SystemConfigDto |      * @memberof SystemConfigDto | ||||||
|      */ |      */ | ||||||
|     'job': SystemConfigJobDto; |     'job': SystemConfigJobDto; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {SystemConfigMachineLearningDto} | ||||||
|  |      * @memberof SystemConfigDto | ||||||
|  |      */ | ||||||
|  |     'machineLearning': SystemConfigMachineLearningDto; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {SystemConfigOAuthDto} |      * @type {SystemConfigOAuthDto} | ||||||
| @@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto { | |||||||
|      */ |      */ | ||||||
|     'videoConversion': JobSettingsDto; |     'videoConversion': JobSettingsDto; | ||||||
| } | } | ||||||
|  | /** | ||||||
|  |  *  | ||||||
|  |  * @export | ||||||
|  |  * @interface SystemConfigMachineLearningDto | ||||||
|  |  */ | ||||||
|  | export interface SystemConfigMachineLearningDto { | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'clipEncodeEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'enabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'facialRecognitionEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'tagImageEnabled': boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof SystemConfigMachineLearningDto | ||||||
|  |      */ | ||||||
|  |     'url': string; | ||||||
|  | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|  * @export |  * @export | ||||||
| @@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); |  | ||||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; |  | ||||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; |  | ||||||
| 
 |  | ||||||
|             return { |  | ||||||
|                 url: toPathString(localVarUrlObj), |  | ||||||
|                 options: localVarRequestOptions, |  | ||||||
|             }; |  | ||||||
|         }, |  | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |  | ||||||
|             const localVarPath = `/search/config`; |  | ||||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 |  | ||||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); |  | ||||||
|             let baseOptions; |  | ||||||
|             if (configuration) { |  | ||||||
|                 baseOptions = configuration.baseOptions; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; |  | ||||||
|             const localVarHeaderParameter = {} as any; |  | ||||||
|             const localVarQueryParameter = {} as any; |  | ||||||
| 
 |  | ||||||
|             // authentication cookie required
 |  | ||||||
| 
 |  | ||||||
|             // authentication api_key required
 |  | ||||||
|             await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) |  | ||||||
| 
 |  | ||||||
|             // authentication bearer required
 |  | ||||||
|             // http bearer authentication required
 |  | ||||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); |             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; |             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; |             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||||
| @@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) { | |||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> { |  | ||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options); |  | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |  | ||||||
|         }, |  | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
|          * @param {string} [q]  |          * @param {string} [q]  | ||||||
| @@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat | |||||||
|         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { |         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { | ||||||
|             return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); |             return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |  | ||||||
|          *  |  | ||||||
|          * @param {*} [options] Override http request option. |  | ||||||
|          * @throws {RequiredError} |  | ||||||
|          */ |  | ||||||
|         getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> { |  | ||||||
|             return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath)); |  | ||||||
|         }, |  | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
|          * @param {SearchApiSearchRequest} requestParameters Request parameters. |          * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||||
| @@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI { | |||||||
|         return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); |         return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      *  |  | ||||||
|      * @param {*} [options] Override http request option. |  | ||||||
|      * @throws {RequiredError} |  | ||||||
|      * @memberof SearchApi |  | ||||||
|      */ |  | ||||||
|     public getSearchConfig(options?: AxiosRequestConfig) { |  | ||||||
|         return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @param {SearchApiSearchRequest} requestParameters Request parameters. |      * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||||
|   | |||||||
| @@ -70,25 +70,26 @@ | |||||||
|       subtitle: 'Discover or synchronize sidecar metadata from the filesystem', |       subtitle: 'Discover or synchronize sidecar metadata from the filesystem', | ||||||
|       allText: 'SYNC', |       allText: 'SYNC', | ||||||
|       missingText: 'DISCOVER', |       missingText: 'DISCOVER', | ||||||
|  |       disabled: !$featureFlags.sidecar, | ||||||
|     }, |     }, | ||||||
|     [JobName.ObjectTagging]: { |     [JobName.ObjectTagging]: { | ||||||
|       icon: TagMultiple, |       icon: TagMultiple, | ||||||
|       title: api.getJobName(JobName.ObjectTagging), |       title: api.getJobName(JobName.ObjectTagging), | ||||||
|       subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected', |       subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected', | ||||||
|       disabled: !$featureFlags.machineLearning, |       disabled: !$featureFlags.tagImage, | ||||||
|     }, |     }, | ||||||
|     [JobName.ClipEncoding]: { |     [JobName.ClipEncoding]: { | ||||||
|       icon: VectorCircle, |       icon: VectorCircle, | ||||||
|       title: api.getJobName(JobName.ClipEncoding), |       title: api.getJobName(JobName.ClipEncoding), | ||||||
|       subtitle: 'Run machine learning to generate clip embeddings', |       subtitle: 'Run machine learning to generate clip embeddings', | ||||||
|       disabled: !$featureFlags.machineLearning, |       disabled: !$featureFlags.clipEncode, | ||||||
|     }, |     }, | ||||||
|     [JobName.RecognizeFaces]: { |     [JobName.RecognizeFaces]: { | ||||||
|       icon: FaceRecognition, |       icon: FaceRecognition, | ||||||
|       title: api.getJobName(JobName.RecognizeFaces), |       title: api.getJobName(JobName.RecognizeFaces), | ||||||
|       subtitle: 'Run machine learning to recognize faces', |       subtitle: 'Run machine learning to recognize faces', | ||||||
|       handleCommand: handleFaceCommand, |       handleCommand: handleFaceCommand, | ||||||
|       disabled: !$featureFlags.machineLearning, |       disabled: !$featureFlags.facialRecognition, | ||||||
|     }, |     }, | ||||||
|     [JobName.VideoConversion]: { |     [JobName.VideoConversion]: { | ||||||
|       icon: Video, |       icon: Video, | ||||||
|   | |||||||
| @@ -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} /> |     <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} /> | ||||||
|  |  | ||||||
|     {#if disabled} |     {#if disabled} | ||||||
|       <span class="slider-disable" /> |       <span class="slider-disable cursor-not-allowed" /> | ||||||
|     {:else} |     {:else} | ||||||
|       <span class="slider" /> |       <span class="slider cursor-pointer" /> | ||||||
|     {/if} |     {/if} | ||||||
|   </label> |   </label> | ||||||
| </div> | </div> | ||||||
| @@ -43,7 +43,6 @@ | |||||||
|   .slider, |   .slider, | ||||||
|   .slider-disable { |   .slider-disable { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     cursor: pointer; |  | ||||||
|     top: 0; |     top: 0; | ||||||
|     left: 0; |     left: 0; | ||||||
|     right: 0; |     right: 0; | ||||||
|   | |||||||
| @@ -4,7 +4,10 @@ import { writable } from 'svelte/store'; | |||||||
| export type FeatureFlags = ServerFeaturesDto; | export type FeatureFlags = ServerFeaturesDto; | ||||||
|  |  | ||||||
| export const featureFlags = writable<FeatureFlags>({ | export const featureFlags = writable<FeatureFlags>({ | ||||||
|   machineLearning: true, |   clipEncode: true, | ||||||
|  |   facialRecognition: true, | ||||||
|  |   sidecar: true, | ||||||
|  |   tagImage: true, | ||||||
|   search: true, |   search: true, | ||||||
|   oauth: true, |   oauth: true, | ||||||
|   oauthAutoLaunch: true, |   oauthAutoLaunch: true, | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ | |||||||
|   import { page } from '$app/stores'; |   import { page } from '$app/stores'; | ||||||
|   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; |   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; | ||||||
|   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; |   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; | ||||||
|   import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; |   import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte'; | ||||||
|   import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; |   import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; | ||||||
|   import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; |   import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; | ||||||
|   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; |   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; | ||||||
|   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; |   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; | ||||||
|  |   import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; | ||||||
|   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; |   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||||
|   import { api } from '@api'; |   import { api } from '@api'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
| @@ -50,6 +51,10 @@ | |||||||
|       <OAuthSettings oauthConfig={configs.oauth} /> |       <OAuthSettings oauthConfig={configs.oauth} /> | ||||||
|     </SettingAccordion> |     </SettingAccordion> | ||||||
|  |  | ||||||
|  |     <SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings"> | ||||||
|  |       <MachineLearningSettings /> | ||||||
|  |     </SettingAccordion> | ||||||
|  |  | ||||||
|     <SettingAccordion |     <SettingAccordion | ||||||
|       title="Storage Template" |       title="Storage Template" | ||||||
|       subtitle="Manage the folder structure and file name of the upload asset" |       subtitle="Manage the folder structure and file name of the upload asset" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user