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
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2066,19 +2066,6 @@ export interface SearchAssetResponseDto { | ||||
|      */ | ||||
|     'total': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SearchConfigResponseDto | ||||
|  */ | ||||
| export interface SearchConfigResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SearchConfigResponseDto | ||||
|      */ | ||||
|     'enabled': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -2185,7 +2172,13 @@ export interface ServerFeaturesDto { | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'machineLearning': boolean; | ||||
|     'clipEncode': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'facialRecognition': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -2210,6 +2203,18 @@ export interface ServerFeaturesDto { | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'search': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'sidecar': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof ServerFeaturesDto | ||||
|      */ | ||||
|     'tagImage': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -2611,6 +2616,12 @@ export interface SystemConfigDto { | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'job': SystemConfigJobDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigMachineLearningDto} | ||||
|      * @memberof SystemConfigDto | ||||
|      */ | ||||
|     'machineLearning': SystemConfigMachineLearningDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {SystemConfigOAuthDto} | ||||
| @@ -2778,6 +2789,43 @@ export interface SystemConfigJobDto { | ||||
|      */ | ||||
|     'videoConversion': JobSettingsDto; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SystemConfigMachineLearningDto | ||||
|  */ | ||||
| export interface SystemConfigMachineLearningDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SystemConfigMachineLearningDto | ||||
|      */ | ||||
|     'clipEncodeEnabled': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SystemConfigMachineLearningDto | ||||
|      */ | ||||
|     'enabled': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SystemConfigMachineLearningDto | ||||
|      */ | ||||
|     'facialRecognitionEnabled': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof SystemConfigMachineLearningDto | ||||
|      */ | ||||
|     'tagImageEnabled': boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof SystemConfigMachineLearningDto | ||||
|      */ | ||||
|     'url': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -10106,44 +10154,6 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/search/config`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication cookie required
 | ||||
| 
 | ||||
|             // authentication api_key required
 | ||||
|             await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
| @@ -10290,15 +10300,6 @@ export const SearchApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} [q]  | ||||
| @@ -10342,14 +10343,6 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat | ||||
|         getExploreData(options?: AxiosRequestConfig): AxiosPromise<Array<SearchExploreResponseDto>> { | ||||
|             return localVarFp.getExploreData(options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getSearchConfig(options?: AxiosRequestConfig): AxiosPromise<SearchConfigResponseDto> { | ||||
|             return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||
| @@ -10498,16 +10491,6 @@ export class SearchApi extends BaseAPI { | ||||
|         return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof SearchApi | ||||
|      */ | ||||
|     public getSearchConfig(options?: AxiosRequestConfig) { | ||||
|         return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {SearchApiSearchRequest} requestParameters Request parameters. | ||||
|   | ||||
| @@ -70,25 +70,26 @@ | ||||
|       subtitle: 'Discover or synchronize sidecar metadata from the filesystem', | ||||
|       allText: 'SYNC', | ||||
|       missingText: 'DISCOVER', | ||||
|       disabled: !$featureFlags.sidecar, | ||||
|     }, | ||||
|     [JobName.ObjectTagging]: { | ||||
|       icon: TagMultiple, | ||||
|       title: api.getJobName(JobName.ObjectTagging), | ||||
|       subtitle: 'Run machine learning to tag objects\nNote that some assets may not have any objects detected', | ||||
|       disabled: !$featureFlags.machineLearning, | ||||
|       disabled: !$featureFlags.tagImage, | ||||
|     }, | ||||
|     [JobName.ClipEncoding]: { | ||||
|       icon: VectorCircle, | ||||
|       title: api.getJobName(JobName.ClipEncoding), | ||||
|       subtitle: 'Run machine learning to generate clip embeddings', | ||||
|       disabled: !$featureFlags.machineLearning, | ||||
|       disabled: !$featureFlags.clipEncode, | ||||
|     }, | ||||
|     [JobName.RecognizeFaces]: { | ||||
|       icon: FaceRecognition, | ||||
|       title: api.getJobName(JobName.RecognizeFaces), | ||||
|       subtitle: 'Run machine learning to recognize faces', | ||||
|       handleCommand: handleFaceCommand, | ||||
|       disabled: !$featureFlags.machineLearning, | ||||
|       disabled: !$featureFlags.facialRecognition, | ||||
|     }, | ||||
|     [JobName.VideoConversion]: { | ||||
|       icon: Video, | ||||
|   | ||||
| @@ -0,0 +1,104 @@ | ||||
| <script lang="ts"> | ||||
|   import { | ||||
|     notificationController, | ||||
|     NotificationType, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { api, SystemConfigDto } from '@api'; | ||||
|   import { isEqual } from 'lodash-es'; | ||||
|   import { fade } from 'svelte/transition'; | ||||
|   import SettingButtonsRow from '../setting-buttons-row.svelte'; | ||||
|   import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; | ||||
|   import SettingSwitch from '../setting-switch.svelte'; | ||||
|  | ||||
|   let config: SystemConfigDto; | ||||
|   let defaultConfig: SystemConfigDto; | ||||
|  | ||||
|   async function refreshConfig() { | ||||
|     [config, defaultConfig] = await Promise.all([ | ||||
|       api.systemConfigApi.getConfig().then((res) => res.data), | ||||
|       api.systemConfigApi.getDefaults().then((res) => res.data), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   async function reset() { | ||||
|     const { data: resetConfig } = await api.systemConfigApi.getConfig(); | ||||
|     config = resetConfig; | ||||
|     notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info }); | ||||
|   } | ||||
|  | ||||
|   async function saveSetting() { | ||||
|     try { | ||||
|       const { data: current } = await api.systemConfigApi.getConfig(); | ||||
|       await api.systemConfigApi.updateConfig({ | ||||
|         systemConfigDto: { ...current, machineLearning: config.machineLearning }, | ||||
|       }); | ||||
|       await refreshConfig(); | ||||
|       notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Unable to save settings'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async function resetToDefault() { | ||||
|     await refreshConfig(); | ||||
|     const { data: defaults } = await api.systemConfigApi.getDefaults(); | ||||
|     config = defaults; | ||||
|  | ||||
|     notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info }); | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <div class="mt-2"> | ||||
|   {#await refreshConfig() then} | ||||
|     <div in:fade={{ duration: 500 }}> | ||||
|       <form autocomplete="off" on:submit|preventDefault class="mx-4 flex flex-col gap-4 py-4"> | ||||
|         <SettingSwitch | ||||
|           title="Enabled" | ||||
|           subtitle="Use machine learning features" | ||||
|           bind:checked={config.machineLearning.enabled} | ||||
|         /> | ||||
|  | ||||
|         <hr /> | ||||
|  | ||||
|         <SettingInputField | ||||
|           inputType={SettingInputFieldType.TEXT} | ||||
|           label="URL" | ||||
|           desc="URL of machine learning server" | ||||
|           bind:value={config.machineLearning.url} | ||||
|           required={true} | ||||
|           disabled={!config.machineLearning.enabled} | ||||
|           isEdited={!(config.machineLearning.url === config.machineLearning.url)} | ||||
|         /> | ||||
|  | ||||
|         <SettingSwitch | ||||
|           title="SMART SEARCH" | ||||
|           subtitle="Extract CLIP embeddings for smart search" | ||||
|           bind:checked={config.machineLearning.clipEncodeEnabled} | ||||
|           disabled={!config.machineLearning.enabled} | ||||
|         /> | ||||
|  | ||||
|         <SettingSwitch | ||||
|           title="FACIAL RECOGNITION" | ||||
|           subtitle="Recognize and group faces in photos" | ||||
|           disabled={!config.machineLearning.enabled} | ||||
|           bind:checked={config.machineLearning.facialRecognitionEnabled} | ||||
|         /> | ||||
|  | ||||
|         <SettingSwitch | ||||
|           title="IMAGE TAGGING" | ||||
|           subtitle="Tag and classify images" | ||||
|           disabled={!config.machineLearning.enabled} | ||||
|           bind:checked={config.machineLearning.tagImageEnabled} | ||||
|         /> | ||||
|  | ||||
|         <SettingButtonsRow | ||||
|           on:reset={reset} | ||||
|           on:save={saveSetting} | ||||
|           on:reset-to-default={resetToDefault} | ||||
|           showResetToDefault={!isEqual(config, defaultConfig)} | ||||
|         /> | ||||
|       </form> | ||||
|     </div> | ||||
|   {/await} | ||||
| </div> | ||||
| @@ -32,9 +32,9 @@ | ||||
|     <input class="disabled::cursor-not-allowed h-0 w-0 opacity-0" type="checkbox" bind:checked on:click {disabled} /> | ||||
|  | ||||
|     {#if disabled} | ||||
|       <span class="slider-disable" /> | ||||
|       <span class="slider-disable cursor-not-allowed" /> | ||||
|     {:else} | ||||
|       <span class="slider" /> | ||||
|       <span class="slider cursor-pointer" /> | ||||
|     {/if} | ||||
|   </label> | ||||
| </div> | ||||
| @@ -43,7 +43,6 @@ | ||||
|   .slider, | ||||
|   .slider-disable { | ||||
|     position: absolute; | ||||
|     cursor: pointer; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|   | ||||
| @@ -4,7 +4,10 @@ import { writable } from 'svelte/store'; | ||||
| export type FeatureFlags = ServerFeaturesDto; | ||||
|  | ||||
| export const featureFlags = writable<FeatureFlags>({ | ||||
|   machineLearning: true, | ||||
|   clipEncode: true, | ||||
|   facialRecognition: true, | ||||
|   sidecar: true, | ||||
|   tagImage: true, | ||||
|   search: true, | ||||
|   oauth: true, | ||||
|   oauthAutoLaunch: true, | ||||
|   | ||||
| @@ -2,11 +2,12 @@ | ||||
|   import { page } from '$app/stores'; | ||||
|   import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; | ||||
|   import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; | ||||
|   import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; | ||||
|   import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte'; | ||||
|   import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; | ||||
|   import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; | ||||
|   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; | ||||
|   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; | ||||
|   import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; | ||||
|   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
|   import { api } from '@api'; | ||||
|   import type { PageData } from './$types'; | ||||
| @@ -50,6 +51,10 @@ | ||||
|       <OAuthSettings oauthConfig={configs.oauth} /> | ||||
|     </SettingAccordion> | ||||
|  | ||||
|     <SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings"> | ||||
|       <MachineLearningSettings /> | ||||
|     </SettingAccordion> | ||||
|  | ||||
|     <SettingAccordion | ||||
|       title="Storage Template" | ||||
|       subtitle="Manage the folder structure and file name of the upload asset" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user