scrypted-detection-trainer 0.1.10 β†’ 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- (()=>{var e={562(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),t.sdk=t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,i(n(192),t);const o=n(192);n(339);class s extends o.DeviceBase{constructor(e){super(),this.nativeId=e}get storage(){return this._storage||(this._storage=t.sdk.deviceManager.getDeviceStorage(this.nativeId)),this._storage}get log(){return this._log||(this._log=t.sdk.deviceManager.getDeviceLogger(this.nativeId)),this._log}get console(){return this._console||(this._console=t.sdk.deviceManager.getDeviceConsole(this.nativeId)),this._console}async createMediaObject(e,n){return t.sdk.mediaManager.createMediaObject(e,n,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:t.sdk.deviceManager.getMixinConsole(e.sourceId,this.nativeId)}_lazyLoadDeviceState(){this._deviceState||(this.nativeId?this._deviceState=t.sdk.deviceManager.getDeviceState(this.nativeId):this._deviceState=t.sdk.deviceManager.getDeviceState())}onDeviceEvent(e,n){return t.sdk.deviceManager.onDeviceEvent(this.nativeId,e,n)}}t.ScryptedDeviceBase=s;class r extends o.DeviceBase{constructor(e){super(),this._listeners=new Set,this.mixinDevice=e.mixinDevice,this.mixinDeviceInterfaces=e.mixinDeviceInterfaces,this.mixinStorageSuffix=e.mixinStorageSuffix,this._deviceState=e.mixinDeviceState,this.nativeId=t.sdk.systemManager.getDeviceById(this.id).nativeId,this.mixinProviderNativeId=e.mixinProviderNativeId,this._deviceState.__rpcproxy_traps_all_properties&&"string"==typeof this._deviceState.id&&(this._deviceState=t.sdk.deviceManager.createDeviceState(this._deviceState.id,this._deviceState.setState))}get storage(){if(!this._storage){const e=this.mixinStorageSuffix,n=this.id+(e?":"+e:"");this._storage=t.sdk.deviceManager.getMixinStorage(n,this.mixinProviderNativeId)}return this._storage}get console(){return this._console||(t.sdk.deviceManager.getMixinConsole?this._console=t.sdk.deviceManager.getMixinConsole(this.id,this.mixinProviderNativeId):this._console=t.sdk.deviceManager.getDeviceConsole(this.mixinProviderNativeId)),this._console}async createMediaObject(e,n){return t.sdk.mediaManager.createMediaObject(e,n,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:t.sdk.deviceManager.getMixinConsole(e.sourceId,this.mixinProviderNativeId)}onDeviceEvent(e,n){return t.sdk.deviceManager.onMixinEvent(this.id,this,e,n)}_lazyLoadDeviceState(){}manageListener(e){this._listeners.add(e)}release(){for(const e of this._listeners)e.removeListener()}}t.MixinDeviceBase=r,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState?.[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState?this._deviceState[e]=t:console.warn("device state is unavailable. the device must be discovered with deviceManager.onDeviceDiscovered or deviceManager.onDevicesChanged before the state can be set.")}}for(const n of Object.values(o.ScryptedInterfaceProperty))n!==o.ScryptedInterfaceProperty.nativeId&&(Object.defineProperty(s.prototype,n,{set:t(n),get:e(n)}),Object.defineProperty(r.prototype,n,{set:t(n),get:e(n)}))}(),t.sdk={};try{let e=!1;try{process.env.SCRYPTED_SDK_ES_MODULE||process.env.SCRYPTED_SDK_MODULE;const a=process.env.SCRYPTED_SDK_CJS_MODULE||process.env.SCRYPTED_SDK_MODULE;if(a)if("undefined"!=typeof require){const n=require(process.env.SCRYPTED_SDK_MODULE);Object.assign(t.sdk,n.getScryptedStatic()),e=!0}else{const i=n(891)(a);Object.assign(t.sdk,i.getScryptedStatic()),e=!0}}catch(e){throw console.warn("failed to load sdk module",e),e}if(!e){let e;try{e=pluginRuntimeAPI}catch(e){}Object.assign(t.sdk,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI,...e})}try{t.sdk.systemManager.setScryptedInterfaceDescriptors?.(o.TYPES_VERSION,o.ScryptedInterfaceDescriptors)?.catch(()=>{})}catch(e){}}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/client instead",e)}t.default=t.sdk},891(e){function t(e){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}t.keys=()=>[],t.resolve=t,t.id=891,e.exports=t},192(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ScryptedMimeTypes=t.ScryptedInterface=t.MediaPlayerState=t.SecuritySystemObstruction=t.SecuritySystemMode=t.AirQuality=t.AirPurifierMode=t.AirPurifierStatus=t.ChargeState=t.LockState=t.PanTiltZoomMovement=t.ThermostatMode=t.TemperatureUnit=t.FanMode=t.HumidityMode=t.ScryptedDeviceType=t.ScryptedInterfaceDescriptors=t.ScryptedInterfaceMethod=t.ScryptedInterfaceProperty=t.DeviceBase=t.TYPES_VERSION=void 0,t.TYPES_VERSION="0.3.116";var n,a,i,o,s,r,c,d,l,p,m,u,g,v,b,h,f,y;t.DeviceBase=class{},function(e){e.id="id",e.info="info",e.interfaces="interfaces",e.mixins="mixins",e.name="name",e.nativeId="nativeId",e.pluginId="pluginId",e.providedInterfaces="providedInterfaces",e.providedName="providedName",e.providedRoom="providedRoom",e.providedType="providedType",e.providerId="providerId",e.room="room",e.type="type",e.scryptedRuntimeArguments="scryptedRuntimeArguments",e.on="on",e.brightness="brightness",e.colorTemperature="colorTemperature",e.rgb="rgb",e.hsv="hsv",e.buttons="buttons",e.sensors="sensors",e.running="running",e.paused="paused",e.docked="docked",e.temperatureSetting="temperatureSetting",e.temperature="temperature",e.temperatureUnit="temperatureUnit",e.humidity="humidity",e.audioVolumes="audioVolumes",e.recordingActive="recordingActive",e.ptzCapabilities="ptzCapabilities",e.lockState="lockState",e.entryOpen="entryOpen",e.batteryLevel="batteryLevel",e.chargeState="chargeState",e.online="online",e.fromMimeType="fromMimeType",e.toMimeType="toMimeType",e.converters="converters",e.binaryState="binaryState",e.tampered="tampered",e.sleeping="sleeping",e.powerDetected="powerDetected",e.audioDetected="audioDetected",e.motionDetected="motionDetected",e.ambientLight="ambientLight",e.occupied="occupied",e.flooded="flooded",e.ultraviolet="ultraviolet",e.luminance="luminance",e.position="position",e.securitySystemState="securitySystemState",e.pm10Density="pm10Density",e.pm25Density="pm25Density",e.vocDensity="vocDensity",e.noxDensity="noxDensity",e.co2ppm="co2ppm",e.airQuality="airQuality",e.airPurifierState="airPurifierState",e.filterChangeIndication="filterChangeIndication",e.filterLifeLevel="filterLifeLevel",e.humiditySetting="humiditySetting",e.fan="fan",e.applicationInfo="applicationInfo",e.systemDevice="systemDevice"}(n||(t.ScryptedInterfaceProperty=n={})),function(e){e.listen="listen",e.probe="probe",e.setMixins="setMixins",e.setName="setName",e.setRoom="setRoom",e.setType="setType",e.getPluginJson="getPluginJson",e.turnOff="turnOff",e.turnOn="turnOn",e.setBrightness="setBrightness",e.getTemperatureMaxK="getTemperatureMaxK",e.getTemperatureMinK="getTemperatureMinK",e.setColorTemperature="setColorTemperature",e.setRgb="setRgb",e.setHsv="setHsv",e.pressButton="pressButton",e.sendNotification="sendNotification",e.start="start",e.stop="stop",e.pause="pause",e.resume="resume",e.dock="dock",e.setTemperature="setTemperature",e.setTemperatureUnit="setTemperatureUnit",e.getPictureOptions="getPictureOptions",e.takePicture="takePicture",e.getAudioStream="getAudioStream",e.setAudioVolumes="setAudioVolumes",e.startDisplay="startDisplay",e.stopDisplay="stopDisplay",e.getVideoStream="getVideoStream",e.getVideoStreamOptions="getVideoStreamOptions",e.getPrivacyMasks="getPrivacyMasks",e.setPrivacyMasks="setPrivacyMasks",e.getVideoTextOverlays="getVideoTextOverlays",e.setVideoTextOverlay="setVideoTextOverlay",e.getRecordingStream="getRecordingStream",e.getRecordingStreamCurrentTime="getRecordingStreamCurrentTime",e.getRecordingStreamOptions="getRecordingStreamOptions",e.getRecordingStreamThumbnail="getRecordingStreamThumbnail",e.deleteRecordingStream="deleteRecordingStream",e.setRecordingActive="setRecordingActive",e.ptzCommand="ptzCommand",e.getRecordedEvents="getRecordedEvents",e.getVideoClip="getVideoClip",e.getVideoClips="getVideoClips",e.getVideoClipThumbnail="getVideoClipThumbnail",e.removeVideoClips="removeVideoClips",e.setVideoStreamOptions="setVideoStreamOptions",e.startIntercom="startIntercom",e.stopIntercom="stopIntercom",e.lock="lock",e.unlock="unlock",e.addPassword="addPassword",e.getPasswords="getPasswords",e.removePassword="removePassword",e.activate="activate",e.deactivate="deactivate",e.isReversible="isReversible",e.closeEntry="closeEntry",e.openEntry="openEntry",e.getDevice="getDevice",e.releaseDevice="releaseDevice",e.adoptDevice="adoptDevice",e.discoverDevices="discoverDevices",e.createDevice="createDevice",e.getCreateDeviceSettings="getCreateDeviceSettings",e.reboot="reboot",e.getRefreshFrequency="getRefreshFrequency",e.refresh="refresh",e.getMediaStatus="getMediaStatus",e.load="load",e.seek="seek",e.skipNext="skipNext",e.skipPrevious="skipPrevious",e.convert="convert",e.convertMedia="convertMedia",e.getSettings="getSettings",e.putSetting="putSetting",e.armSecuritySystem="armSecuritySystem",e.disarmSecuritySystem="disarmSecuritySystem",e.setAirPurifierState="setAirPurifierState",e.getReadmeMarkdown="getReadmeMarkdown",e.getOauthUrl="getOauthUrl",e.onOauthCallback="onOauthCallback",e.canMixin="canMixin",e.getMixin="getMixin",e.releaseMixin="releaseMixin",e.onRequest="onRequest",e.onConnection="onConnection",e.onPush="onPush",e.run="run",e.eval="eval",e.loadScripts="loadScripts",e.saveScript="saveScript",e.forkInterface="forkInterface",e.trackObjects="trackObjects",e.getDetectionInput="getDetectionInput",e.getObjectTypes="getObjectTypes",e.detectObjects="detectObjects",e.generateObjectDetections="generateObjectDetections",e.getDetectionModel="getDetectionModel",e.setHumidity="setHumidity",e.setFan="setFan",e.startRTCSignalingSession="startRTCSignalingSession",e.createRTCSignalingSession="createRTCSignalingSession",e.getScryptedUserAccessControl="getScryptedUserAccessControl",e.generateVideoFrames="generateVideoFrames",e.connectStream="connectStream",e.getTTYSettings="getTTYSettings"}(a||(t.ScryptedInterfaceMethod=a={})),t.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setMixins","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","nativeId","pluginId","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},ScryptedPluginRuntime:{name:"ScryptedPluginRuntime",methods:[],properties:["scryptedRuntimeArguments"]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Buttons:{name:"Buttons",methods:[],properties:["buttons"]},PressButtons:{name:"PressButtons",methods:["pressButton"],properties:[]},Sensors:{name:"Sensors",methods:[],properties:["sensors"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setTemperature"],properties:["temperatureSetting"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},Microphone:{name:"Microphone",methods:["getAudioStream"],properties:[]},AudioVolumeControl:{name:"AudioVolumeControl",methods:["setAudioVolumes"],properties:["audioVolumes"]},Display:{name:"Display",methods:["startDisplay","stopDisplay"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraMask:{name:"VideoCameraMask",methods:["getPrivacyMasks","setPrivacyMasks"],properties:[]},VideoTextOverlays:{name:"VideoTextOverlays",methods:["getVideoTextOverlays","setVideoTextOverlay"],properties:[]},VideoRecorder:{name:"VideoRecorder",methods:["getRecordingStream","getRecordingStreamCurrentTime","getRecordingStreamOptions","getRecordingStreamThumbnail"],properties:["recordingActive"]},VideoRecorderManagement:{name:"VideoRecorderManagement",methods:["deleteRecordingStream","setRecordingActive"],properties:[]},PanTiltZoom:{name:"PanTiltZoom",methods:["ptzCommand"],properties:["ptzCapabilities"]},EventRecorder:{name:"EventRecorder",methods:["getRecordedEvents"],properties:[]},VideoClips:{name:"VideoClips",methods:["getVideoClip","getVideoClips","getVideoClipThumbnail","removeVideoClips"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice","releaseDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["adoptDevice","discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Charger:{name:"Charger",methods:[],properties:["chargeState"]},Reboot:{name:"Reboot",methods:["reboot"],properties:[]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},MediaConverter:{name:"MediaConverter",methods:["convertMedia"],properties:["converters"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},TamperSensor:{name:"TamperSensor",methods:[],properties:["tampered"]},Sleep:{name:"Sleep",methods:[],properties:["sleeping"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},SecuritySystem:{name:"SecuritySystem",methods:["armSecuritySystem","disarmSecuritySystem"],properties:["securitySystemState"]},PM10Sensor:{name:"PM10Sensor",methods:[],properties:["pm10Density"]},PM25Sensor:{name:"PM25Sensor",methods:[],properties:["pm25Density"]},VOCSensor:{name:"VOCSensor",methods:[],properties:["vocDensity"]},NOXSensor:{name:"NOXSensor",methods:[],properties:["noxDensity"]},CO2Sensor:{name:"CO2Sensor",methods:[],properties:["co2ppm"]},AirQualitySensor:{name:"AirQualitySensor",methods:[],properties:["airQuality"]},AirPurifier:{name:"AirPurifier",methods:["setAirPurifierState"],properties:["airPurifierState"]},FilterMaintenance:{name:"FilterMaintenance",methods:[],properties:["filterChangeIndication","filterLifeLevel"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ClusterForkInterface:{name:"ClusterForkInterface",methods:["forkInterface"],properties:[]},ObjectTracker:{name:"ObjectTracker",methods:["trackObjects"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","generateObjectDetections","getDetectionModel"],properties:[]},ObjectDetectionPreview:{name:"ObjectDetectionPreview",methods:[],properties:[]},ObjectDetectionGenerator:{name:"ObjectDetectionGenerator",methods:[],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]},RTCSignalingClient:{name:"RTCSignalingClient",methods:["createRTCSignalingSession"],properties:[]},LauncherApplication:{name:"LauncherApplication",methods:[],properties:["applicationInfo"]},ScryptedUser:{name:"ScryptedUser",methods:["getScryptedUserAccessControl"],properties:[]},VideoFrameGenerator:{name:"VideoFrameGenerator",methods:["generateVideoFrames"],properties:[]},StreamService:{name:"StreamService",methods:["connectStream"],properties:[]},TTY:{name:"TTY",methods:[],properties:[]},TTYSettings:{name:"TTYSettings",methods:["getTTYSettings"],properties:[]},ScryptedSystemDevice:{name:"ScryptedSystemDevice",methods:[],properties:["systemDevice"]},ScryptedDeviceCreator:{name:"ScryptedDeviceCreator",methods:[],properties:[]},ScryptedSettings:{name:"ScryptedSettings",methods:[],properties:[]}},function(e){e.Builtin="Builtin",e.Internal="Internal",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.SmartDisplay="SmartDisplay",e.Speaker="Speaker",e.SmartSpeaker="SmartSpeaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.SecuritySystem="SecuritySystem",e.WindowCovering="WindowCovering",e.Siren="Siren",e.AirPurifier="AirPurifier",e.Internet="Internet",e.Network="Network",e.Bridge="Bridge",e.Unknown="Unknown"}(i||(t.ScryptedDeviceType=i={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(o||(t.HumidityMode=o={})),function(e){e.Auto="Auto",e.Manual="Manual"}(s||(t.FanMode=s={})),function(e){e.C="C",e.F="F"}(r||(t.TemperatureUnit=r={})),function(e){e.Off="Off",e.Cool="Cool",e.Heat="Heat",e.HeatCool="HeatCool",e.Auto="Auto",e.FanOnly="FanOnly",e.Purifier="Purifier",e.Eco="Eco",e.Dry="Dry",e.On="On"}(c||(t.ThermostatMode=c={})),function(e){e.Absolute="Absolute",e.Relative="Relative",e.Continuous="Continuous",e.Preset="Preset",e.Home="Home"}(d||(t.PanTiltZoomMovement=d={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(l||(t.LockState=l={})),function(e){e.Trickle="trickle",e.Charging="charging",e.NotCharging="not-charging"}(p||(t.ChargeState=p={})),function(e){e.Inactive="Inactive",e.Idle="Idle",e.Active="Active",e.ActiveNightMode="ActiveNightMode"}(m||(t.AirPurifierStatus=m={})),function(e){e.Manual="Manual",e.Automatic="Automatic"}(u||(t.AirPurifierMode=u={})),function(e){e.Unknown="Unknown",e.Excellent="Excellent",e.Good="Good",e.Fair="Fair",e.Inferior="Inferior",e.Poor="Poor"}(g||(t.AirQuality=g={})),function(e){e.Disarmed="Disarmed",e.HomeArmed="HomeArmed",e.AwayArmed="AwayArmed",e.NightArmed="NightArmed"}(v||(t.SecuritySystemMode=v={})),function(e){e.Sensor="Sensor",e.Occupied="Occupied",e.Time="Time",e.Error="Error"}(b||(t.SecuritySystemObstruction=b={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(h||(t.MediaPlayerState=h={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.ScryptedPluginRuntime="ScryptedPluginRuntime",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Buttons="Buttons",e.PressButtons="PressButtons",e.Sensors="Sensors",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.Microphone="Microphone",e.AudioVolumeControl="AudioVolumeControl",e.Display="Display",e.VideoCamera="VideoCamera",e.VideoCameraMask="VideoCameraMask",e.VideoTextOverlays="VideoTextOverlays",e.VideoRecorder="VideoRecorder",e.VideoRecorderManagement="VideoRecorderManagement",e.PanTiltZoom="PanTiltZoom",e.EventRecorder="EventRecorder",e.VideoClips="VideoClips",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Charger="Charger",e.Reboot="Reboot",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.BufferConverter="BufferConverter",e.MediaConverter="MediaConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.TamperSensor="TamperSensor",e.Sleep="Sleep",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.SecuritySystem="SecuritySystem",e.PM10Sensor="PM10Sensor",e.PM25Sensor="PM25Sensor",e.VOCSensor="VOCSensor",e.NOXSensor="NOXSensor",e.CO2Sensor="CO2Sensor",e.AirQualitySensor="AirQualitySensor",e.AirPurifier="AirPurifier",e.FilterMaintenance="FilterMaintenance",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ClusterForkInterface="ClusterForkInterface",e.ObjectTracker="ObjectTracker",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.ObjectDetectionPreview="ObjectDetectionPreview",e.ObjectDetectionGenerator="ObjectDetectionGenerator",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel",e.RTCSignalingClient="RTCSignalingClient",e.LauncherApplication="LauncherApplication",e.ScryptedUser="ScryptedUser",e.VideoFrameGenerator="VideoFrameGenerator",e.StreamService="StreamService",e.TTY="TTY",e.TTYSettings="TTYSettings",e.ScryptedSystemDevice="ScryptedSystemDevice",e.ScryptedDeviceCreator="ScryptedDeviceCreator",e.ScryptedSettings="ScryptedSettings"}(f||(t.ScryptedInterface=f={})),function(e){e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.ServerId="text/x-server-id",e.PushEndpoint="text/x-push-endpoint",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaStreamUrl="text/x-media-url",e.MediaObject="x-scrypted/x-scrypted-media-object",e.RequestMediaObject="x-scrypted/x-scrypted-request-media-object",e.RequestMediaStream="x-scrypted/x-scrypted-request-stream",e.MediaStreamFeedback="x-scrypted/x-media-stream-feedback",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.FFmpegTranscodeStream="x-scrypted/x-ffmpeg-transcode-stream",e.RTCSignalingChannel="x-scrypted/x-scrypted-rtc-signaling-channel",e.RTCSignalingSession="x-scrypted/x-scrypted-rtc-signaling-session",e.RTCConnectionManagement="x-scrypted/x-scrypted-rtc-connection-management",e.Image="x-scrypted/x-scrypted-image"}(y||(t.ScryptedMimeTypes=y={}))},927(e,t,n){"use strict";var a,i=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),o=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||(a=function(e){return a=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t},a(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n=a(e),s=0;s<n.length;s++)"default"!==n[s]&&i(t,e,n[s]);return o(t,e),t}),r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const c=s(n(562)),d=r(n(896)),l=r(n(928)),{systemManager:p,deviceManager:m,mediaManager:u}=c.default,g=new Set(["person","cat","dog","animal","bird","face","vehicle","car","truck","bus","motorcycle","bicycle","plate","package"]),v=["disabled","1 per minute","1 per 10 seconds","every detection"],b={disabled:1/0,"1 per minute":6e4,"1 per 10 seconds":1e4,"every detection":0};class h extends c.ScryptedDeviceBase{constructor(e){super(e),this.lastCapture=new Map,this.captures=new Map,this.listeners=[],this.imgDir=l.default.join(process.env.SCRYPTED_PLUGIN_VOLUME||"/tmp","detection-trainer-images");try{d.default.mkdirSync(this.imgDir,{recursive:!0})}catch{}this.loadState(),this.registerListeners()}loadState(){try{const e=this.storage.getItem("captures");if(e){const t=JSON.parse(e);for(const e of t)this.captures.set(e.id,e);this.console.log(`Loaded ${this.captures.size} captures from storage.`)}}catch(e){this.console.warn("Could not load captures from storage:",e)}for(const[e]of this.captures)try{this.storage.removeItem(`img:${e}`)}catch{}}saveCaptures(){try{this.storage.setItem("captures",JSON.stringify([...this.captures.values()]))}catch(e){this.console.warn("Could not save captures:",e)}}imgPath(e){return l.default.join(this.imgDir,`${e}.jpg`)}saveImage(e,t){try{d.default.writeFileSync(this.imgPath(e),t)}catch(t){this.console.warn(`Could not save image ${e}:`,t)}}loadImage(e){try{const t=this.imgPath(e);if(d.default.existsSync(t))return d.default.readFileSync(t)}catch{}}deleteCapture(e){try{d.default.unlinkSync(this.imgPath(e))}catch{}this.captures.delete(e),this.saveCaptures()}async getSettings(){const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector));let t;try{t=await c.default.endpointManager.getLocalEndpoint(void 0,{public:!0})}catch{t="/endpoint/scrypted-detection-trainer/public/"}const n=[{key:"info",title:"Detection Trainer",description:`${this.captures.size} captures stored (${[...this.captures.values()].filter(e=>!e.reviewed).length} pending review, ${[...this.captures.values()].filter(e=>e.reviewed&&"discard"!==e.label).length} labeled).`,readonly:!0,value:""},{key:"open_ui",title:"Review UI",description:"Open the detection review and labeling interface.",type:"html",readonly:!0,value:`<a href="${t}" target="_blank" style="display:inline-block;padding:8px 16px;background:#1a4d8a;color:#fff;border-radius:6px;text-decoration:none;font-size:13px;">Open Review UI β†—</a>`},{key:"autoCapture",title:"Auto-Capture",description:"Automatically capture detections in the background. Disable to use manual browsing only.",type:"boolean",value:this.storage.getItem("autoCapture")??"true"}];for(const t of e){const e=`rate:${t.id}`;n.push({key:e,title:t.name,group:"Capture Rate per Camera",description:"How often to capture detections from this camera.",value:this.storage.getItem(e)||"1 per minute",choices:[...v]})}return n}async putSetting(e,t){"open_ui"!==e&&"ui_link"!==e&&"info"!==e&&(this.storage.setItem(e,t),e.startsWith("rate:")&&this.registerListeners())}registerListeners(){for(const e of this.listeners)e();this.listeners=[];const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector));for(const t of e){const e=`rate:${t.id}`,n=this.storage.getItem(e)||"1 per minute";if("disabled"===n)continue;const a=t.listen(c.ScryptedInterface.ObjectDetector,async(e,a,i)=>{await this.onDetection(t.id,t.name,i,b[n])});this.listeners.push(()=>a.removeListener())}this.console.log(`Listening to ${this.listeners.length} camera(s).`)}async onDetection(e,t,n,a){if("false"===(this.storage.getItem("autoCapture")??"true"))return;if(!n?.detections?.length||!n.inputDimensions)return;const i=Date.now();if(i-(this.lastCapture.get(e)||0)<a)return;const o=n.detections.filter(e=>e.className&&g.has(e.className.toLowerCase())&&e.boundingBox);if(!o.length)return;const s=o.sort((e,t)=>(t.score||0)-(e.score||0))[0];if(this.captures.size>=2e3){const e=[...this.captures.values()].filter(e=>!e.reviewed).sort((e,t)=>e.timestamp-t.timestamp)[0];if(!e)return;this.deleteCapture(e.id)}let r;this.lastCapture.set(e,i);try{if(n.detectionId){const t=p.getDeviceById(e),a=await t.getDetectionInput(n.detectionId);r=await u.convertMediaObjectToBuffer(a,"image/jpeg")}}catch(e){this.console.warn(`Could not get detection image for ${t}:`,e)}if(!r)return;const c=`${i}-${Math.random().toString(36).slice(2,8)}`,d={id:c,cameraId:e,cameraName:t,timestamp:i,detectedClass:s.className,score:s.score||0,boundingBox:s.boundingBox,inputDimensions:n.inputDimensions,detectionId:n.detectionId,reviewed:!1};this.captures.set(c,d),this.saveImage(c,r),this.saveCaptures(),this.console.log(`Captured ${s.className} (${Math.round(100*(s.score||0))}%) from ${t} [${this.captures.size} total]`)}async onRequest(e,t){const n=new URL(e.url,"http://localhost").pathname.replace(e.rootPath,"");if("/api/browse-img"===n){const n=new URL(e.url,"http://localhost").searchParams,a=n.get("cameraId")?.replace(/[^a-zA-Z0-9_\-]/g,""),i=n.get("detectionId")?.replace(/[^a-zA-Z0-9_\-]/g,"");if(!a||!i)return t.send("Missing params",{code:400});try{const e=p.getDeviceById(a),n=await e.getDetectionInput(i),o=await u.convertMediaObjectToBuffer(n,"image/jpeg");return t.send(o,{headers:{"Content-Type":"image/jpeg","Cache-Control":"max-age=60"}})}catch(e){return t.send("Image unavailable",{code:404})}}if(n.startsWith("/img/")){const e=n.slice(5).replace(/[^a-zA-Z0-9_\-]/g,""),a=this.loadImage(e);return a?t.send(a,{headers:{"Content-Type":"image/jpeg","Cache-Control":"max-age=3600"}}):t.send("Not found",{code:404})}if("/api/label"===n&&e.body){const n=e.body,a=JSON.parse("string"==typeof n?n:Buffer.isBuffer(n)?n.toString():String(n)),i=this.captures.get(a.id);return i?(i.label=a.label,i.reviewed=!0,"discard"===a.label?this.deleteCapture(a.id):(this.captures.set(a.id,i),this.saveCaptures()),t.send(JSON.stringify({ok:!0}),{headers:{"Content-Type":"application/json"}})):t.send("Not found",{code:404})}if("/api/cameras"===n){const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector)).map(e=>({id:e.id,name:e.name}));return t.send(JSON.stringify(e),{headers:{"Content-Type":"application/json"}})}if("/api/browse"===n){const n=new URL(e.url,"http://localhost").searchParams,a=n.get("cameraId"),i=parseInt(n.get("hours")||"24");if(!a)return t.send("Missing cameraId",{code:400});try{const e=p.getDeviceById(a);if(!e)return t.send("Camera not found",{code:404});const n=Date.now(),o=n-3600*i*1e3,s=(await e.getRecordedEvents({startTime:o,endTime:n})||[]).filter(e=>"ObjectDetector"===e.details?.eventInterface&&e.data?.detections?.length).slice(0,100).map(t=>({detectionId:t.data?.detectionId,timestamp:t.details?.eventTime||t.data?.timestamp,detections:(t.data?.detections||[]).map(e=>({className:e.className,score:e.score,boundingBox:e.boundingBox})),inputDimensions:t.data?.inputDimensions,cameraId:a,cameraName:e.name})).filter(e=>e.detectionId&&e.inputDimensions);return t.send(JSON.stringify(s),{headers:{"Content-Type":"application/json"}})}catch(e){return t.send(JSON.stringify({error:e.message}),{headers:{"Content-Type":"application/json"},code:500})}}if("/api/add-event"===n&&e.body){const n=e.body,a=JSON.parse("string"==typeof n?n:Buffer.isBuffer(n)?n.toString():String(n)),{cameraId:i,cameraName:o,detectionId:s,timestamp:r,detectedClass:c,score:d,boundingBox:l,inputDimensions:m,label:g}=a;if(!g||"discard"===g)return t.send(JSON.stringify({ok:!0}),{headers:{"Content-Type":"application/json"}});let v;try{const e=p.getDeviceById(i),t=await e.getDetectionInput(s);v=await u.convertMediaObjectToBuffer(t,"image/jpeg")}catch(e){this.console.warn(`Could not get image for browse event ${s}:`,e)}if(!v)return t.send(JSON.stringify({error:"Could not retrieve image"}),{headers:{"Content-Type":"application/json"},code:500});const b=`browse-${r}-${Math.random().toString(36).slice(2,6)}`,h={id:b,cameraId:i,cameraName:o,timestamp:r,detectedClass:c,score:d,boundingBox:l,inputDimensions:m,detectionId:s,reviewed:!0,label:g};return this.captures.set(b,h),this.saveImage(b,v),this.saveCaptures(),t.send(JSON.stringify({ok:!0,id:b}),{headers:{"Content-Type":"application/json"}})}if("/api/pending"===n){const e=[...this.captures.values()].filter(e=>!e.reviewed).sort((e,t)=>t.timestamp-e.timestamp).slice(0,50);return t.send(JSON.stringify(e),{headers:{"Content-Type":"application/json"}})}if("/api/labeled"===n){const n=parseInt(new URL(e.url,"http://localhost").searchParams.get("page")||"0"),a=50,i=[...this.captures.values()].filter(e=>e.reviewed).sort((e,t)=>t.timestamp-e.timestamp),o=i.slice(n*a,(n+1)*a);return t.send(JSON.stringify({items:o,total:i.length,page:n,pageSize:a}),{headers:{"Content-Type":"application/json"}})}if("/api/stats"===n){const e=[...this.captures.values()],n={total:e.length,pending:e.filter(e=>!e.reviewed).length,labeled:e.filter(e=>e.reviewed&&"discard"!==e.label).length,byLabel:{},byCamera:{},byDetectedClass:{}};for(const t of e)t.label&&(n.byLabel[t.label]=(n.byLabel[t.label]||0)+1),n.byCamera[t.cameraName]=(n.byCamera[t.cameraName]||0)+1,n.byDetectedClass[t.detectedClass]=(n.byDetectedClass[t.detectedClass]||0)+1;return t.send(JSON.stringify(n),{headers:{"Content-Type":"application/json"}})}if("/api/export"===n){const e=[...this.captures.values()].filter(e=>e.reviewed&&e.label&&"discard"!==e.label);if(!e.length)return t.send(JSON.stringify({error:"No labeled data yet"}),{headers:{"Content-Type":"application/json"},code:400});const n={person:0,animal:1,face:2,vehicle:3,plate:4,package:5,discard:-1},a=[];for(const t of e){const e=this.loadImage(t.id);if(!e)continue;const i=`${t.id}`;a.push({filename:`images/${i}.jpg`,content:e.toString("base64"),encoding:"base64"});const[o,s,r,c]=t.boundingBox,[d,l]=t.inputDimensions,p=(o+r/2)/d,m=(s+c/2)/l,u=r/d,g=c/l,v=`${n[t.label]} ${p.toFixed(6)} ${m.toFixed(6)} ${u.toFixed(6)} ${g.toFixed(6)}\n`;a.push({filename:`labels/${i}.txt`,content:v,encoding:"utf8"})}const i=["path: dataset","train: images","val: images","","nc: 6","names: ['person', 'animal', 'face', 'vehicle', 'plate', 'package']","","# Generated by Scrypted Detection Trainer",`# ${e.length} labeled samples`].join("\n");return a.push({filename:"data.yaml",content:i,encoding:"utf8"}),t.send(JSON.stringify({files:a,count:e.length}),{headers:{"Content-Type":"application/json"}})}if("/"===n||""===n||"/index.html"===n)return t.send(this.renderUI(),{headers:{"Content-Type":"text/html"}});t.send("Not found",{code:404})}renderUI(){return"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Detection Trainer</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js\"><\/script>\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e8e8e8; min-height: 100vh; }\n header { background: #1a1a1a; border-bottom: 1px solid #333; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }\n header h1 { font-size: 18px; font-weight: 600; color: #fff; }\n .stats { display: flex; gap: 20px; font-size: 13px; color: #aaa; }\n .stat span { color: #fff; font-weight: 600; }\n .container { max-width: 1000px; margin: 0 auto; padding: 24px; }\n .card { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 12px; overflow: hidden; margin-bottom: 24px; }\n .card-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; display: flex; align-items: center; justify-content: space-between; }\n .card-header h2 { font-size: 15px; font-weight: 600; }\n .badge { background: #333; color: #aaa; font-size: 12px; padding: 2px 8px; border-radius: 20px; }\n .badge.orange { background: #3d2a00; color: #f90; }\n .badge.green { background: #0d2d0d; color: #4c4; }\n\n /* Detection card */\n .detection { display: grid; grid-template-columns: 420px 1fr; gap: 0; border-bottom: 1px solid #222; }\n .detection:last-child { border-bottom: none; }\n .detection-imgs { display: flex; gap: 8px; padding: 10px; background: #111; align-items: center; }\n .img-panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }\n .img-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: .5px; }\n .det-canvas { border-radius: 6px; display: block; }\n .det-class-badge { display: inline-block; background: #3d2a00; color: #f90; font-size: 11px; padding: 2px 8px; border-radius: 4px; }\n .detection-info { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }\n .detection-meta { font-size: 12px; color: #888; display: flex; flex-wrap: wrap; gap: 10px; }\n .detection-meta strong { color: #ccc; }\n .label-buttons { display: flex; flex-wrap: wrap; gap: 8px; }\n .label-btn { padding: 7px 14px; border-radius: 8px; border: 1px solid #444; background: #222; color: #ccc; cursor: pointer; font-size: 13px; transition: all .15s; }\n .label-btn:hover { border-color: #666; background: #2a2a2a; color: #fff; }\n .label-btn.person { border-color: #2a6; color: #4d9; }\n .label-btn.person:hover { background: #0d2a1a; }\n .label-btn.animal { border-color: #a63; color: #d85; }\n .label-btn.animal:hover { background: #2a1a0d; }\n .label-btn.face { border-color: #49c; color: #6be; }\n .label-btn.face:hover { background: #0d1a2a; }\n .label-btn.vehicle { border-color: #76b; color: #99d; }\n .label-btn.vehicle:hover { background: #1a1a2a; }\n .label-btn.discard { border-color: #622; color: #a44; }\n .label-btn.discard:hover { background: #2a0d0d; }\n .detection.labeled { opacity: 0.4; pointer-events: none; }\n .labeled-tag { font-size: 11px; color: #4d9; background: #0d2a1a; border: 1px solid #2a6; padding: 2px 8px; border-radius: 4px; }\n\n /* Empty state */\n .empty { padding: 48px; text-align: center; color: #555; }\n .empty .icon { font-size: 48px; margin-bottom: 12px; }\n\n /* Export section */\n .export-btn { padding: 10px 20px; background: #1a4d8a; border: none; border-radius: 8px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500; }\n .export-btn:hover { background: #1e5ca0; }\n .export-btn:disabled { background: #333; color: #666; cursor: not-allowed; }\n .export-info { font-size: 13px; color: #888; padding: 12px 20px; }\n\n /* Progress bar */\n .progress { height: 4px; background: #222; border-radius: 2px; overflow: hidden; margin-top: 8px; }\n .progress-bar { height: 100%; background: #1a6; border-radius: 2px; transition: width .3s; }\n\n .toast { position: fixed; bottom: 24px; right: 24px; background: #1a3; color: #fff; padding: 10px 18px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .3s; pointer-events: none; }\n .toast.show { opacity: 1; }\n\n .tab-bar { display: flex; gap: 2px; padding: 12px 20px 0; border-bottom: 1px solid #2a2a2a; }\n .tab { padding: 8px 14px; font-size: 13px; color: #888; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }\n .tab.active { color: #fff; border-bottom-color: #4a9; }\n .tab-content { padding: 20px; }\n .tab-panel { display: none; }\n .tab-panel.active { display: block; }\n\n .breakdown-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }\n .breakdown-item { background: #222; border-radius: 8px; padding: 12px; }\n .breakdown-item .label { font-size: 12px; color: #888; margin-bottom: 4px; }\n .breakdown-item .value { font-size: 20px; font-weight: 600; color: #fff; }\n\n /* Lightbox */\n .lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 1000; align-items: center; justify-content: center; flex-direction: column; gap: 12px; }\n .lightbox.open { display: flex; }\n .lightbox canvas { max-width: 92vw; max-height: 82vh; border-radius: 8px; cursor: zoom-out; }\n .lightbox-meta { color: #aaa; font-size: 13px; text-align: center; }\n .lightbox-close { position: absolute; top: 16px; right: 20px; font-size: 28px; color: #888; cursor: pointer; line-height: 1; }\n .lightbox-close:hover { color: #fff; }\n .det-canvas { cursor: zoom-in; }\n</style>\n</head>\n<body>\n<header>\n <h1>🎯 Detection Trainer</h1>\n <div class=\"stats\">\n <div>Pending <span id=\"stat-pending\">β€”</span></div>\n <div>Labeled <span id=\"stat-labeled\">β€”</span></div>\n <div>Total <span id=\"stat-total\">β€”</span></div>\n </div>\n</header>\n<div class=\"container\">\n\n <div class=\"card\">\n <div class=\"tab-bar\">\n <div class=\"tab active\" onclick=\"showTab('review')\">Review</div>\n <div class=\"tab\" onclick=\"showTab('browse')\">Browse Events</div>\n <div class=\"tab\" onclick=\"showTab('labeled')\">Labeled</div>\n <div class=\"tab\" onclick=\"showTab('stats')\">Stats</div>\n <div class=\"tab\" onclick=\"showTab('export')\">Export Dataset</div>\n </div>\n\n \x3c!-- Review tab --\x3e\n <div class=\"tab-panel active\" id=\"tab-review\">\n <div id=\"detections-list\"></div>\n </div>\n\n \x3c!-- Browse tab --\x3e\n <div class=\"tab-panel\" id=\"tab-browse\">\n <div class=\"tab-content\">\n <div style=\"display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px;\">\n <select id=\"browse-camera\" style=\"padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;\">\n <option value=\"\">Select camera…</option>\n </select>\n <select id=\"browse-hours\" style=\"padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;\">\n <option value=\"1\">Last 1 hour</option>\n <option value=\"6\">Last 6 hours</option>\n <option value=\"24\" selected>Last 24 hours</option>\n <option value=\"72\">Last 3 days</option>\n </select>\n <button class=\"export-btn\" onclick=\"loadBrowse()\" style=\"padding:8px 16px;\">Load Events</button>\n <span id=\"browse-status\" style=\"font-size:13px;color:#888;\"></span>\n </div>\n <div id=\"browse-list\"></div>\n </div>\n </div>\n\n \x3c!-- Labeled tab --\x3e\n <div class=\"tab-panel\" id=\"tab-labeled\">\n <div id=\"labeled-list\"></div>\n </div>\n <div class=\"tab-panel\" id=\"tab-stats\">\n <div class=\"tab-content\">\n <p style=\"font-size:13px;color:#888;margin-bottom:16px;\">Breakdown of captured and labeled detections.</p>\n <h3 style=\"font-size:13px;color:#aaa;margin-bottom:10px;\">By Detected Class (what the model said)</h3>\n <div class=\"breakdown-grid\" id=\"stats-detected\"></div>\n <h3 style=\"font-size:13px;color:#aaa;margin:20px 0 10px;\">By Corrected Label (what you said)</h3>\n <div class=\"breakdown-grid\" id=\"stats-label\"></div>\n <h3 style=\"font-size:13px;color:#aaa;margin:20px 0 10px;\">By Camera</h3>\n <div class=\"breakdown-grid\" id=\"stats-camera\"></div>\n </div>\n </div>\n\n \x3c!-- Export tab --\x3e\n <div class=\"tab-panel\" id=\"tab-export\">\n <div class=\"tab-content\">\n <p style=\"font-size:13px;color:#888;margin-bottom:16px;\">\n Exports a YOLO-format dataset (images + labels + data.yaml) as a downloadable bundle.\n Only labeled detections are included. Review more detections first to build a larger dataset.\n </p>\n <div id=\"export-stats\" class=\"export-info\">Loading…</div>\n <div style=\"display:flex;gap:12px;align-items:center;margin-top:12px;\">\n <button class=\"export-btn\" id=\"export-btn\" onclick=\"exportDataset()\">Download Dataset</button>\n <span id=\"export-status\" style=\"font-size:13px;color:#888;\"></span>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<div class=\"lightbox\" id=\"lightbox\" onclick=\"closeLightbox()\">\n <div class=\"lightbox-close\" onclick=\"closeLightbox()\">βœ•</div>\n <canvas id=\"lightbox-canvas\"></canvas>\n <div class=\"lightbox-meta\" id=\"lightbox-meta\"></div>\n</div>\n\n<div class=\"toast\" id=\"toast\"></div>\n\n<script>\nconst BASE = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname;\nlet pending = [];\nlet labeledCount = 0;\n\nfunction imgError(img) {\n img.parentElement.innerHTML = '<div style=\"padding:20px;color:#555;font-size:12px;text-align:center\">No image</div>';\n}\n\nfunction drawDetection(img, r) {\n const [bx, by, bw, bh] = r.boundingBox;\n const [iw, ih] = r.inputDimensions;\n\n // --- Full frame canvas with box overlay ---\n const fullCanvas = document.getElementById('canvas-full-' + r.id);\n if (fullCanvas) {\n const cw = fullCanvas.width, ch = fullCanvas.height;\n const scale = Math.min(cw / iw, ch / ih);\n const dw = iw * scale, dh = ih * scale;\n const ox = (cw - dw) / 2, oy = (ch - dh) / 2;\n const ctx = fullCanvas.getContext('2d');\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, cw, ch);\n ctx.drawImage(img, ox, oy, dw, dh);\n // Draw bounding box\n const rx = ox + bx * scale, ry = oy + by * scale;\n const rw = bw * scale, rh = bh * scale;\n ctx.strokeStyle = '#f90';\n ctx.lineWidth = 2;\n ctx.strokeRect(rx, ry, rw, rh);\n // Label badge\n ctx.fillStyle = 'rgba(255,153,0,0.85)';\n ctx.fillRect(rx, ry - 18, rw, 18);\n ctx.fillStyle = '#000';\n ctx.font = 'bold 11px sans-serif';\n ctx.fillText(r.detectedClass + ' ' + Math.round(r.score * 100) + '%', rx + 3, ry - 4);\n }\n\n // --- Crop canvas ---\n const cropCanvas = document.getElementById('canvas-crop-' + r.id);\n if (cropCanvas) {\n const cc = cropCanvas.width, ch2 = cropCanvas.height;\n const ctx2 = cropCanvas.getContext('2d');\n ctx2.fillStyle = '#111';\n ctx2.fillRect(0, 0, cc, ch2);\n // Add padding around the crop\n const pad = Math.min(bw, bh) * 0.15;\n const sx = Math.max(0, bx - pad), sy = Math.max(0, by - pad);\n const sw = Math.min(iw - sx, bw + pad * 2), sh = Math.min(ih - sy, bh + pad * 2);\n const scale2 = Math.min(cc / sw, ch2 / sh);\n const dw2 = sw * scale2, dh2 = sh * scale2;\n const ox2 = (cc - dw2) / 2, oy2 = (ch2 - dh2) / 2;\n ctx2.drawImage(img, sx, sy, sw, sh, ox2, oy2, dw2, dh2);\n // Thin box outline on crop\n ctx2.strokeStyle = '#f90';\n ctx2.lineWidth = 1.5;\n const rx2 = ox2 + pad * scale2, ry2 = oy2 + pad * scale2;\n ctx2.strokeRect(rx2, ry2, bw * scale2, bh * scale2);\n }\n}\n\n// Cache loaded images so lightbox reuses them\nconst imgCache = new Map();\n\nfunction openLightbox(r) {\n const img = imgCache.get(r.id);\n if (!img) return;\n\n const lb = document.getElementById('lightbox');\n const lbCanvas = document.getElementById('lightbox-canvas');\n\n // Size canvas to image, capped at viewport\n const maxW = window.innerWidth * 0.9;\n const maxH = window.innerHeight * 0.8;\n const [iw, ih] = r.inputDimensions;\n const scale = Math.min(maxW / iw, maxH / ih, 1);\n lbCanvas.width = Math.round(iw * scale);\n lbCanvas.height = Math.round(ih * scale);\n\n const ctx = lbCanvas.getContext('2d');\n ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);\n\n // Draw all bounding boxes for this detection (primary + others in same event if available)\n const boxes = r.allDetections || [{ boundingBox: r.boundingBox, detectedClass: r.detectedClass, score: r.score }];\n const colors = ['#f90', '#4af', '#f44', '#4f4', '#f4f', '#ff4'];\n boxes.forEach((d, i) => {\n const [bx, by, bw, bh] = d.boundingBox;\n const color = colors[i % colors.length];\n const rx = bx * scale, ry = by * scale, rw = bw * scale, rh = bh * scale;\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.strokeRect(rx, ry, rw, rh);\n const label = d.detectedClass + ' ' + Math.round((d.score || 0) * 100) + '%';\n const textW = ctx.measureText(label).width + 8;\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.85;\n ctx.fillRect(rx, Math.max(0, ry - 20), textW, 20);\n ctx.globalAlpha = 1;\n ctx.fillStyle = '#000';\n ctx.font = 'bold 12px sans-serif';\n ctx.fillText(label, rx + 4, Math.max(14, ry - 4));\n });\n\n document.getElementById('lightbox-meta').textContent =\n r.cameraName + ' Β· ' + new Date(r.timestamp).toLocaleString() + ' Β· ' + iw + 'Γ—' + ih;\n lb.classList.add('open');\n document.addEventListener('keydown', lbKeyHandler);\n}\n\nfunction closeLightbox() {\n document.getElementById('lightbox').classList.remove('open');\n document.removeEventListener('keydown', lbKeyHandler);\n}\n\nfunction lbKeyHandler(e) {\n if (e.key === 'Escape') closeLightbox();\n}\n\nfunction showTab(name) {\n const names = ['review', 'browse', 'labeled', 'stats', 'export'];\n document.querySelectorAll('.tab').forEach((t, i) => {\n t.classList.toggle('active', names[i] === name);\n });\n document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));\n document.getElementById('tab-' + name).classList.add('active');\n if (name === 'stats') loadStats();\n if (name === 'export') loadExportInfo();\n if (name === 'labeled') loadLabeled(0);\n if (name === 'browse') initBrowse();\n}\n\nasync function initBrowse() {\n const sel = document.getElementById('browse-camera');\n if (sel.options.length > 1) return; // already loaded\n try {\n const res = await fetch(BASE + '/api/cameras');\n const cameras = await res.json();\n for (const cam of cameras) {\n const opt = document.createElement('option');\n opt.value = cam.id;\n opt.textContent = cam.name;\n sel.appendChild(opt);\n }\n if (cameras.length === 1) sel.value = cameras[0].id;\n } catch(e) {\n document.getElementById('browse-status').textContent = 'Error loading cameras';\n }\n}\n\nasync function loadBrowse() {\n const cameraId = document.getElementById('browse-camera').value;\n const hours = document.getElementById('browse-hours').value;\n const status = document.getElementById('browse-status');\n const list = document.getElementById('browse-list');\n\n if (!cameraId) { status.textContent = 'Select a camera first'; return; }\n\n status.textContent = 'Loading…';\n list.innerHTML = '';\n\n try {\n const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);\n const events = await res.json();\n\n if (events.error) { status.textContent = 'Error: ' + events.error; return; }\n if (!events.length) { status.textContent = 'No detection events found.'; list.innerHTML = '<div class=\"empty\"><div class=\"icon\">πŸ”</div><div>No ObjectDetector events in this time range.</div></div>'; return; }\n\n status.textContent = events.length + ' events found';\n\n list.innerHTML = events.map((ev, i) => {\n const date = new Date(ev.timestamp).toLocaleString();\n const dets = ev.detections || [];\n const primary = dets[0] || {};\n const score = Math.round((primary.score || 0) * 100);\n const allClasses = dets.map(d => d.className + ' ' + Math.round((d.score||0)*100) + '%').join(', ');\n return `\n <div class=\"detection\" id=\"bev-${i}\" style=\"opacity:1;transition:opacity .3s\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"bcanvas-${i}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\" id=\"bcrop-panel-${i}\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"bcanvas-crop-${i}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${ev.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${allClasses}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Add to dataset as:</div>\n <div class=\"label-buttons\" id=\"blabels-${i}\">\n <button class=\"label-btn person\" onclick=\"addEvent(${i})('person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"addEvent(${i})('animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"addEvent(${i})('face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"addEvent(${i})('vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"addEvent(${i})('plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"addEvent(${i})('package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"addEvent(${i})('discard')\">πŸ—‘ Skip</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Load images for each event\n for (let i = 0; i < events.length; i++) {\n const ev = events[i];\n loadBrowseImage(i, ev);\n }\n\n } catch(e) {\n status.textContent = 'Error: ' + e.message;\n }\n}\n\n// Store browse events for addEvent closure\nlet browseEvents = [];\n\nasync function loadBrowse() {\n const cameraId = document.getElementById('browse-camera').value;\n const hours = document.getElementById('browse-hours').value;\n const status = document.getElementById('browse-status');\n const list = document.getElementById('browse-list');\n\n if (!cameraId) { status.textContent = 'Select a camera first'; return; }\n\n status.textContent = 'Loading…';\n list.innerHTML = '';\n browseEvents = [];\n\n try {\n const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);\n const events = await res.json();\n\n if (events.error) { status.textContent = 'Error: ' + events.error; return; }\n if (!events.length) {\n status.textContent = 'No detection events found.';\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">πŸ”</div><div>No ObjectDetector events in this time range.</div></div>';\n return;\n }\n\n browseEvents = events;\n status.textContent = events.length + ' events';\n\n list.innerHTML = events.map((ev, i) => {\n const date = new Date(ev.timestamp).toLocaleString();\n const dets = ev.detections || [];\n const allClasses = [...new Set(dets.map(d => d.className))].join(', ');\n return `\n <div class=\"detection\" id=\"bev-${i}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"bcanvas-full-${i}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"bcanvas-crop-${i}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${ev.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${allClasses}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Add to dataset as:</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"addBrowseEvent(${i},'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"addBrowseEvent(${i},'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"addBrowseEvent(${i},'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"addBrowseEvent(${i},'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"addBrowseEvent(${i},'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"addBrowseEvent(${i},'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"addBrowseEvent(${i},'discard')\">πŸ—‘ Skip</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Load thumbnails for each event\n for (let i = 0; i < events.length; i++) {\n loadBrowseImage(i, events[i]);\n }\n\n } catch(e) {\n status.textContent = 'Error: ' + e.message;\n }\n}\n\nfunction loadBrowseImage(i, ev) {\n const primary = (ev.detections || [])[0];\n if (!primary?.boundingBox) return;\n // Request image via the img endpoint using detectionId as the key\n // We store a browse-prefixed image server-side only after adding β€” for preview\n // use a placeholder fetch to trigger server-side caching\n fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&detectionId=' + ev.detectionId)\n .then(r => r.ok ? r.blob() : null)\n .then(blob => {\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const img = new Image();\n img.onload = () => {\n imgCache.set('browse-' + i, img);\n // Draw on full canvas\n const fullCanvas = document.getElementById('bcanvas-full-' + i);\n const cropCanvas = document.getElementById('bcanvas-crop-' + i);\n if (fullCanvas) fullCanvas.id = 'canvas-full-browse' + i;\n if (cropCanvas) cropCanvas.id = 'canvas-crop-browse' + i;\n const fakeR = { ...ev, id: 'browse' + i, boundingBox: primary.boundingBox, detectedClass: primary.className, score: primary.score };\n drawDetection(img, fakeR);\n if (fullCanvas) { fullCanvas.id = 'bcanvas-full-' + i; fullCanvas.onclick = () => openLightbox(fakeR); }\n if (cropCanvas) { cropCanvas.id = 'bcanvas-crop-' + i; cropCanvas.onclick = () => openLightbox(fakeR); }\n URL.revokeObjectURL(url);\n };\n img.src = url;\n }).catch(() => {});\n}\n\nasync function addBrowseEvent(i, label) {\n const ev = browseEvents[i];\n if (!ev) return;\n const el = document.getElementById('bev-' + i);\n if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }\n\n if (label !== 'discard') {\n const primary = (ev.detections || [])[0];\n if (!primary) return;\n try {\n const res = await fetch(BASE + '/api/add-event', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n cameraId: ev.cameraId,\n cameraName: ev.cameraName,\n detectionId: ev.detectionId,\n timestamp: ev.timestamp,\n detectedClass: primary.className,\n score: primary.score,\n boundingBox: primary.boundingBox,\n inputDimensions: ev.inputDimensions,\n label,\n }),\n });\n const data = await res.json();\n if (data.error) { toast('Error: ' + data.error, '#633'); if (el) el.style.opacity = '1'; el?.querySelectorAll('button').forEach(b => b.disabled = false); return; }\n toast('Added: ' + label, '#1a6');\n } catch(e) {\n toast('Failed: ' + e.message, '#633');\n if (el) el.style.opacity = '1';\n el?.querySelectorAll('button').forEach(b => b.disabled = false);\n return;\n }\n } else {\n toast('Skipped', '#555');\n }\n\n // Remove from list after short delay\n setTimeout(() => { if (el) el.remove(); }, 400);\n\n // Update stats\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n}\n\nconst LABEL_COLORS = { person:'#4d9', animal:'#d85', face:'#6be', vehicle:'#99d', plate:'#cc9', package:'#fc9', discard:'#a44' };\n\nasync function loadLabeled(page) {\n const list = document.getElementById('labeled-list');\n list.innerHTML = '<div class=\"empty\"><div style=\"color:#888\">Loading…</div></div>';\n try {\n const res = await fetch(BASE + '/api/labeled?page=' + page);\n const data = await res.json();\n const { items, total, pageSize } = data;\n const totalPages = Math.ceil(total / pageSize);\n\n if (!items.length) {\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">🏷️</div><div>No labeled detections yet.</div></div>';\n return;\n }\n\n const pagerHtml = totalPages > 1 ? `\n <div style=\"display:flex;align-items:center;gap:12px;padding:14px 16px;border-top:1px solid #222;font-size:13px;color:#888;\">\n ${page > 0 ? `<button class=\"label-btn\" onclick=\"loadLabeled(${page-1})\">← Prev</button>` : ''}\n <span>Page ${page+1} of ${totalPages} Β· ${total} total</span>\n ${page < totalPages-1 ? `<button class=\"label-btn\" onclick=\"loadLabeled(${page+1})\">Next β†’</button>` : ''}\n </div>` : '';\n\n list.innerHTML = items.map(r => {\n const date = new Date(r.timestamp).toLocaleString();\n const score = Math.round(r.score * 100);\n const labelColor = LABEL_COLORS[r.label] || '#aaa';\n return `\n <div class=\"detection\" id=\"ldet-${r.id}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"lcanvas-full-${r.id}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"lcanvas-crop-${r.id}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${r.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${r.detectedClass} ${score}%</div>\n <div style=\"display:inline-block;background:#1a1a1a;border:1px solid ${labelColor};color:${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;\">βœ“ ${r.label}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Change label:</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"relabel('${r.id}', 'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"relabel('${r.id}', 'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"relabel('${r.id}', 'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"relabel('${r.id}', 'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"relabel('${r.id}', 'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"relabel('${r.id}', 'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"relabel('${r.id}', 'discard')\">πŸ—‘ Discard</button>\n </div>\n </div>\n </div>`;\n }).join('') + pagerHtml;\n\n // Draw bounding boxes\n for (const r of items) {\n const img = new Image();\n img.onload = () => {\n imgCache.set(r.id, img);\n // Reuse drawDetection with the labeled canvases\n const origFull = document.getElementById('canvas-full-' + r.id);\n const origCrop = document.getElementById('canvas-crop-' + r.id);\n // Temporarily point to labeled canvases\n const fakeFull = document.getElementById('lcanvas-full-' + r.id);\n const fakeCrop = document.getElementById('lcanvas-crop-' + r.id);\n if (fakeFull) fakeFull.id = 'canvas-full-' + r.id;\n if (fakeCrop) fakeCrop.id = 'canvas-crop-' + r.id;\n drawDetection(img, r);\n if (fakeFull) fakeFull.id = 'lcanvas-full-' + r.id;\n if (fakeCrop) fakeCrop.id = 'lcanvas-crop-' + r.id;\n if (fakeFull) fakeFull.onclick = () => openLightbox(r);\n if (fakeCrop) fakeCrop.onclick = () => openLightbox(r);\n };\n img.src = BASE + '/img/' + r.id;\n }\n } catch(e) {\n list.innerHTML = '<div class=\"empty\"><div style=\"color:#a44\">Error: ' + e.message + '</div></div>';\n }\n}\n\nasync function relabel(id, labelVal) {\n await fetch(BASE + '/api/label', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, label: labelVal }),\n });\n const el = document.getElementById('ldet-' + id);\n if (labelVal === 'discard') {\n if (el) el.remove();\n toast('Discarded', '#633');\n } else {\n // Update the label badge in place\n const badge = el && el.querySelector('[style*=\"βœ“\"]');\n const labelColor = LABEL_COLORS[labelVal] || '#aaa';\n if (el) {\n const badges = el.querySelectorAll('.detection-meta > div');\n badges.forEach(b => { if (b.textContent.startsWith('βœ“')) b.remove(); });\n const meta = el.querySelector('.detection-meta');\n const newBadge = document.createElement('div');\n newBadge.style.cssText = `display:inline-block;background:#1a1a1a;border:1px solid ${labelColor};color:${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;`;\n newBadge.textContent = 'βœ“ ' + labelVal;\n meta.appendChild(newBadge);\n }\n toast('Re-labeled: ' + labelVal, '#1a6');\n }\n // Refresh stats\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-pending').textContent = stats.pending;\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n}\n\nfunction toast(msg, color='#1a3') {\n const el = document.getElementById('toast');\n el.textContent = msg;\n el.style.background = color;\n el.classList.add('show');\n setTimeout(() => el.classList.remove('show'), 2500);\n}\n\nasync function loadPending() {\n try {\n const res = await fetch(BASE + '/api/pending');\n pending = await res.json();\n\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-pending').textContent = stats.pending;\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n\n const list = document.getElementById('detections-list');\n if (!pending.length) {\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">βœ…</div><div>No pending detections to review.<br><span style=\"font-size:12px;color:#444\">Captures will appear here as cameras detect objects.</span></div></div>';\n return;\n }\n\n list.innerHTML = pending.map(r => {\n const date = new Date(r.timestamp).toLocaleString();\n const score = Math.round(r.score * 100);\n const bb = r.boundingBox;\n const dim = r.inputDimensions;\n return `\n <div class=\"detection\" id=\"det-${r.id}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"canvas-full-${r.id}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"canvas-crop-${r.id}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${r.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${r.detectedClass} ${score}%</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">What is this actually?</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"label('${r.id}', 'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"label('${r.id}', 'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"label('${r.id}', 'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"label('${r.id}', 'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"label('${r.id}', 'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"label('${r.id}', 'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"label('${r.id}', 'discard')\">πŸ—‘ Discard</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Draw bounding boxes and crops onto canvases after DOM is ready\n for (const r of pending) {\n const img = new Image();\n img.onload = () => {\n imgCache.set(r.id, img);\n drawDetection(img, r);\n // Wire up click to open lightbox\n const fullCanvas = document.getElementById('canvas-full-' + r.id);\n const cropCanvas = document.getElementById('canvas-crop-' + r.id);\n if (fullCanvas) fullCanvas.onclick = () => openLightbox(r);\n if (cropCanvas) cropCanvas.onclick = () => openLightbox(r);\n };\n img.onerror = () => {\n const c = document.getElementById('canvas-full-' + r.id);\n if (c) c.parentElement.innerHTML = '<div style=\"padding:10px;color:#555;font-size:11px\">No image</div>';\n };\n img.src = BASE + '/img/' + r.id;\n } } catch(e) {\n console.error('loadPending error', e);\n const list = document.getElementById('detections-list');\n if (list) list.innerHTML = '<div class=\"empty\"><div style=\"color:#a44\">Error loading captures: ' + e.message + '</div></div>';\n }\n}\n\nasync function label(id, labelVal) {\n const el = document.getElementById('det-' + id);\n if (el) {\n el.classList.add('labeled');\n const btns = el.querySelectorAll('.label-btn');\n btns.forEach(b => b.disabled = true);\n }\n await fetch(BASE + '/api/label', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, label: labelVal }),\n });\n toast(labelVal === 'discard' ? 'Discarded' : 'Labeled: ' + labelVal, labelVal === 'discard' ? '#633' : '#1a6');\n setTimeout(() => {\n if (el) el.remove();\n loadPending();\n }, 600);\n}\n\nasync function loadStats() {\n const res = await fetch(BASE + '/api/stats');\n const stats = await res.json();\n\n const renderBreakdown = (obj, container) => {\n const el = document.getElementById(container);\n const entries = Object.entries(obj).sort((a, b) => b[1] - a[1]);\n el.innerHTML = entries.length\n ? entries.map(([k, v]) => `<div class=\"breakdown-item\"><div class=\"label\">${k}</div><div class=\"value\">${v}</div></div>`).join('')\n : '<div style=\"color:#555;font-size:13px;\">None yet</div>';\n };\n\n renderBreakdown(stats.byDetectedClass, 'stats-detected');\n renderBreakdown(stats.byLabel, 'stats-label');\n renderBreakdown(stats.byCamera, 'stats-camera');\n}\n\nasync function loadExportInfo() {\n const res = await fetch(BASE + '/api/stats');\n const stats = await res.json();\n document.getElementById('export-stats').textContent =\n `${stats.labeled} labeled samples ready for export across ${Object.keys(stats.byCamera).length} camera(s).`;\n}\n\nasync function exportDataset() {\n const btn = document.getElementById('export-btn');\n const status = document.getElementById('export-status');\n btn.disabled = true;\n status.textContent = 'Fetching data…';\n\n try {\n const res = await fetch(BASE + '/api/export');\n if (!res.ok) { status.textContent = 'Nothing to export yet.'; btn.disabled = false; return; }\n const data = await res.json();\n if (data.error) { status.textContent = data.error; btn.disabled = false; return; }\n\n status.textContent = 'Building zip…';\n\n const zip = new JSZip();\n for (const f of data.files) {\n if (f.encoding === 'base64') {\n zip.file(f.filename, f.content, { base64: true });\n } else {\n zip.file(f.filename, f.content);\n }\n }\n\n const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.zip';\n a.click();\n URL.revokeObjectURL(url);\n status.textContent = `Downloaded ${data.count} samples.`;\n toast('Dataset downloaded!');\n } catch (e) {\n status.textContent = 'Export failed: ' + e.message;\n }\n btn.disabled = false;\n}\n\n// Initial load\nloadPending();\n// Auto-refresh pending every 30s\nsetInterval(loadPending, 30_000);\n<\/script>\n</body>\n</html>"}}t.default=h},896(e){"use strict";e.exports=require("fs")},339(e){"use strict";e.exports=require("module")},928(e){"use strict";e.exports=require("path")}},t={};function n(a){var i=t[a];if(void 0!==i)return i.exports;var o=t[a]={exports:{}};return e[a].call(o.exports,o,o.exports,n),o.exports}n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var a=n(927),i=exports="undefined"==typeof exports?{}:exports;for(var o in a)i[o]=a[o];a.__esModule&&Object.defineProperty(i,"__esModule",{value:!0})})();
1
+ (()=>{var e={562(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),t.sdk=t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,i(n(192),t);const o=n(192);n(339);class s extends o.DeviceBase{constructor(e){super(),this.nativeId=e}get storage(){return this._storage||(this._storage=t.sdk.deviceManager.getDeviceStorage(this.nativeId)),this._storage}get log(){return this._log||(this._log=t.sdk.deviceManager.getDeviceLogger(this.nativeId)),this._log}get console(){return this._console||(this._console=t.sdk.deviceManager.getDeviceConsole(this.nativeId)),this._console}async createMediaObject(e,n){return t.sdk.mediaManager.createMediaObject(e,n,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:t.sdk.deviceManager.getMixinConsole(e.sourceId,this.nativeId)}_lazyLoadDeviceState(){this._deviceState||(this.nativeId?this._deviceState=t.sdk.deviceManager.getDeviceState(this.nativeId):this._deviceState=t.sdk.deviceManager.getDeviceState())}onDeviceEvent(e,n){return t.sdk.deviceManager.onDeviceEvent(this.nativeId,e,n)}}t.ScryptedDeviceBase=s;class r extends o.DeviceBase{constructor(e){super(),this._listeners=new Set,this.mixinDevice=e.mixinDevice,this.mixinDeviceInterfaces=e.mixinDeviceInterfaces,this.mixinStorageSuffix=e.mixinStorageSuffix,this._deviceState=e.mixinDeviceState,this.nativeId=t.sdk.systemManager.getDeviceById(this.id).nativeId,this.mixinProviderNativeId=e.mixinProviderNativeId,this._deviceState.__rpcproxy_traps_all_properties&&"string"==typeof this._deviceState.id&&(this._deviceState=t.sdk.deviceManager.createDeviceState(this._deviceState.id,this._deviceState.setState))}get storage(){if(!this._storage){const e=this.mixinStorageSuffix,n=this.id+(e?":"+e:"");this._storage=t.sdk.deviceManager.getMixinStorage(n,this.mixinProviderNativeId)}return this._storage}get console(){return this._console||(t.sdk.deviceManager.getMixinConsole?this._console=t.sdk.deviceManager.getMixinConsole(this.id,this.mixinProviderNativeId):this._console=t.sdk.deviceManager.getDeviceConsole(this.mixinProviderNativeId)),this._console}async createMediaObject(e,n){return t.sdk.mediaManager.createMediaObject(e,n,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:t.sdk.deviceManager.getMixinConsole(e.sourceId,this.mixinProviderNativeId)}onDeviceEvent(e,n){return t.sdk.deviceManager.onMixinEvent(this.id,this,e,n)}_lazyLoadDeviceState(){}manageListener(e){this._listeners.add(e)}release(){for(const e of this._listeners)e.removeListener()}}t.MixinDeviceBase=r,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState?.[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState?this._deviceState[e]=t:console.warn("device state is unavailable. the device must be discovered with deviceManager.onDeviceDiscovered or deviceManager.onDevicesChanged before the state can be set.")}}for(const n of Object.values(o.ScryptedInterfaceProperty))n!==o.ScryptedInterfaceProperty.nativeId&&(Object.defineProperty(s.prototype,n,{set:t(n),get:e(n)}),Object.defineProperty(r.prototype,n,{set:t(n),get:e(n)}))}(),t.sdk={};try{let e=!1;try{process.env.SCRYPTED_SDK_ES_MODULE||process.env.SCRYPTED_SDK_MODULE;const a=process.env.SCRYPTED_SDK_CJS_MODULE||process.env.SCRYPTED_SDK_MODULE;if(a)if("undefined"!=typeof require){const n=require(process.env.SCRYPTED_SDK_MODULE);Object.assign(t.sdk,n.getScryptedStatic()),e=!0}else{const i=n(891)(a);Object.assign(t.sdk,i.getScryptedStatic()),e=!0}}catch(e){throw console.warn("failed to load sdk module",e),e}if(!e){let e;try{e=pluginRuntimeAPI}catch(e){}Object.assign(t.sdk,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI,...e})}try{t.sdk.systemManager.setScryptedInterfaceDescriptors?.(o.TYPES_VERSION,o.ScryptedInterfaceDescriptors)?.catch(()=>{})}catch(e){}}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/client instead",e)}t.default=t.sdk},891(e){function t(e){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}t.keys=()=>[],t.resolve=t,t.id=891,e.exports=t},192(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ScryptedMimeTypes=t.ScryptedInterface=t.MediaPlayerState=t.SecuritySystemObstruction=t.SecuritySystemMode=t.AirQuality=t.AirPurifierMode=t.AirPurifierStatus=t.ChargeState=t.LockState=t.PanTiltZoomMovement=t.ThermostatMode=t.TemperatureUnit=t.FanMode=t.HumidityMode=t.ScryptedDeviceType=t.ScryptedInterfaceDescriptors=t.ScryptedInterfaceMethod=t.ScryptedInterfaceProperty=t.DeviceBase=t.TYPES_VERSION=void 0,t.TYPES_VERSION="0.3.116";var n,a,i,o,s,r,c,d,l,p,m,u,g,b,v,h,f,y;t.DeviceBase=class{},function(e){e.id="id",e.info="info",e.interfaces="interfaces",e.mixins="mixins",e.name="name",e.nativeId="nativeId",e.pluginId="pluginId",e.providedInterfaces="providedInterfaces",e.providedName="providedName",e.providedRoom="providedRoom",e.providedType="providedType",e.providerId="providerId",e.room="room",e.type="type",e.scryptedRuntimeArguments="scryptedRuntimeArguments",e.on="on",e.brightness="brightness",e.colorTemperature="colorTemperature",e.rgb="rgb",e.hsv="hsv",e.buttons="buttons",e.sensors="sensors",e.running="running",e.paused="paused",e.docked="docked",e.temperatureSetting="temperatureSetting",e.temperature="temperature",e.temperatureUnit="temperatureUnit",e.humidity="humidity",e.audioVolumes="audioVolumes",e.recordingActive="recordingActive",e.ptzCapabilities="ptzCapabilities",e.lockState="lockState",e.entryOpen="entryOpen",e.batteryLevel="batteryLevel",e.chargeState="chargeState",e.online="online",e.fromMimeType="fromMimeType",e.toMimeType="toMimeType",e.converters="converters",e.binaryState="binaryState",e.tampered="tampered",e.sleeping="sleeping",e.powerDetected="powerDetected",e.audioDetected="audioDetected",e.motionDetected="motionDetected",e.ambientLight="ambientLight",e.occupied="occupied",e.flooded="flooded",e.ultraviolet="ultraviolet",e.luminance="luminance",e.position="position",e.securitySystemState="securitySystemState",e.pm10Density="pm10Density",e.pm25Density="pm25Density",e.vocDensity="vocDensity",e.noxDensity="noxDensity",e.co2ppm="co2ppm",e.airQuality="airQuality",e.airPurifierState="airPurifierState",e.filterChangeIndication="filterChangeIndication",e.filterLifeLevel="filterLifeLevel",e.humiditySetting="humiditySetting",e.fan="fan",e.applicationInfo="applicationInfo",e.systemDevice="systemDevice"}(n||(t.ScryptedInterfaceProperty=n={})),function(e){e.listen="listen",e.probe="probe",e.setMixins="setMixins",e.setName="setName",e.setRoom="setRoom",e.setType="setType",e.getPluginJson="getPluginJson",e.turnOff="turnOff",e.turnOn="turnOn",e.setBrightness="setBrightness",e.getTemperatureMaxK="getTemperatureMaxK",e.getTemperatureMinK="getTemperatureMinK",e.setColorTemperature="setColorTemperature",e.setRgb="setRgb",e.setHsv="setHsv",e.pressButton="pressButton",e.sendNotification="sendNotification",e.start="start",e.stop="stop",e.pause="pause",e.resume="resume",e.dock="dock",e.setTemperature="setTemperature",e.setTemperatureUnit="setTemperatureUnit",e.getPictureOptions="getPictureOptions",e.takePicture="takePicture",e.getAudioStream="getAudioStream",e.setAudioVolumes="setAudioVolumes",e.startDisplay="startDisplay",e.stopDisplay="stopDisplay",e.getVideoStream="getVideoStream",e.getVideoStreamOptions="getVideoStreamOptions",e.getPrivacyMasks="getPrivacyMasks",e.setPrivacyMasks="setPrivacyMasks",e.getVideoTextOverlays="getVideoTextOverlays",e.setVideoTextOverlay="setVideoTextOverlay",e.getRecordingStream="getRecordingStream",e.getRecordingStreamCurrentTime="getRecordingStreamCurrentTime",e.getRecordingStreamOptions="getRecordingStreamOptions",e.getRecordingStreamThumbnail="getRecordingStreamThumbnail",e.deleteRecordingStream="deleteRecordingStream",e.setRecordingActive="setRecordingActive",e.ptzCommand="ptzCommand",e.getRecordedEvents="getRecordedEvents",e.getVideoClip="getVideoClip",e.getVideoClips="getVideoClips",e.getVideoClipThumbnail="getVideoClipThumbnail",e.removeVideoClips="removeVideoClips",e.setVideoStreamOptions="setVideoStreamOptions",e.startIntercom="startIntercom",e.stopIntercom="stopIntercom",e.lock="lock",e.unlock="unlock",e.addPassword="addPassword",e.getPasswords="getPasswords",e.removePassword="removePassword",e.activate="activate",e.deactivate="deactivate",e.isReversible="isReversible",e.closeEntry="closeEntry",e.openEntry="openEntry",e.getDevice="getDevice",e.releaseDevice="releaseDevice",e.adoptDevice="adoptDevice",e.discoverDevices="discoverDevices",e.createDevice="createDevice",e.getCreateDeviceSettings="getCreateDeviceSettings",e.reboot="reboot",e.getRefreshFrequency="getRefreshFrequency",e.refresh="refresh",e.getMediaStatus="getMediaStatus",e.load="load",e.seek="seek",e.skipNext="skipNext",e.skipPrevious="skipPrevious",e.convert="convert",e.convertMedia="convertMedia",e.getSettings="getSettings",e.putSetting="putSetting",e.armSecuritySystem="armSecuritySystem",e.disarmSecuritySystem="disarmSecuritySystem",e.setAirPurifierState="setAirPurifierState",e.getReadmeMarkdown="getReadmeMarkdown",e.getOauthUrl="getOauthUrl",e.onOauthCallback="onOauthCallback",e.canMixin="canMixin",e.getMixin="getMixin",e.releaseMixin="releaseMixin",e.onRequest="onRequest",e.onConnection="onConnection",e.onPush="onPush",e.run="run",e.eval="eval",e.loadScripts="loadScripts",e.saveScript="saveScript",e.forkInterface="forkInterface",e.trackObjects="trackObjects",e.getDetectionInput="getDetectionInput",e.getObjectTypes="getObjectTypes",e.detectObjects="detectObjects",e.generateObjectDetections="generateObjectDetections",e.getDetectionModel="getDetectionModel",e.setHumidity="setHumidity",e.setFan="setFan",e.startRTCSignalingSession="startRTCSignalingSession",e.createRTCSignalingSession="createRTCSignalingSession",e.getScryptedUserAccessControl="getScryptedUserAccessControl",e.generateVideoFrames="generateVideoFrames",e.connectStream="connectStream",e.getTTYSettings="getTTYSettings"}(a||(t.ScryptedInterfaceMethod=a={})),t.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setMixins","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","nativeId","pluginId","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},ScryptedPluginRuntime:{name:"ScryptedPluginRuntime",methods:[],properties:["scryptedRuntimeArguments"]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Buttons:{name:"Buttons",methods:[],properties:["buttons"]},PressButtons:{name:"PressButtons",methods:["pressButton"],properties:[]},Sensors:{name:"Sensors",methods:[],properties:["sensors"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setTemperature"],properties:["temperatureSetting"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},Microphone:{name:"Microphone",methods:["getAudioStream"],properties:[]},AudioVolumeControl:{name:"AudioVolumeControl",methods:["setAudioVolumes"],properties:["audioVolumes"]},Display:{name:"Display",methods:["startDisplay","stopDisplay"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraMask:{name:"VideoCameraMask",methods:["getPrivacyMasks","setPrivacyMasks"],properties:[]},VideoTextOverlays:{name:"VideoTextOverlays",methods:["getVideoTextOverlays","setVideoTextOverlay"],properties:[]},VideoRecorder:{name:"VideoRecorder",methods:["getRecordingStream","getRecordingStreamCurrentTime","getRecordingStreamOptions","getRecordingStreamThumbnail"],properties:["recordingActive"]},VideoRecorderManagement:{name:"VideoRecorderManagement",methods:["deleteRecordingStream","setRecordingActive"],properties:[]},PanTiltZoom:{name:"PanTiltZoom",methods:["ptzCommand"],properties:["ptzCapabilities"]},EventRecorder:{name:"EventRecorder",methods:["getRecordedEvents"],properties:[]},VideoClips:{name:"VideoClips",methods:["getVideoClip","getVideoClips","getVideoClipThumbnail","removeVideoClips"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice","releaseDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["adoptDevice","discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Charger:{name:"Charger",methods:[],properties:["chargeState"]},Reboot:{name:"Reboot",methods:["reboot"],properties:[]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},MediaConverter:{name:"MediaConverter",methods:["convertMedia"],properties:["converters"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},TamperSensor:{name:"TamperSensor",methods:[],properties:["tampered"]},Sleep:{name:"Sleep",methods:[],properties:["sleeping"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},SecuritySystem:{name:"SecuritySystem",methods:["armSecuritySystem","disarmSecuritySystem"],properties:["securitySystemState"]},PM10Sensor:{name:"PM10Sensor",methods:[],properties:["pm10Density"]},PM25Sensor:{name:"PM25Sensor",methods:[],properties:["pm25Density"]},VOCSensor:{name:"VOCSensor",methods:[],properties:["vocDensity"]},NOXSensor:{name:"NOXSensor",methods:[],properties:["noxDensity"]},CO2Sensor:{name:"CO2Sensor",methods:[],properties:["co2ppm"]},AirQualitySensor:{name:"AirQualitySensor",methods:[],properties:["airQuality"]},AirPurifier:{name:"AirPurifier",methods:["setAirPurifierState"],properties:["airPurifierState"]},FilterMaintenance:{name:"FilterMaintenance",methods:[],properties:["filterChangeIndication","filterLifeLevel"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ClusterForkInterface:{name:"ClusterForkInterface",methods:["forkInterface"],properties:[]},ObjectTracker:{name:"ObjectTracker",methods:["trackObjects"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","generateObjectDetections","getDetectionModel"],properties:[]},ObjectDetectionPreview:{name:"ObjectDetectionPreview",methods:[],properties:[]},ObjectDetectionGenerator:{name:"ObjectDetectionGenerator",methods:[],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]},RTCSignalingClient:{name:"RTCSignalingClient",methods:["createRTCSignalingSession"],properties:[]},LauncherApplication:{name:"LauncherApplication",methods:[],properties:["applicationInfo"]},ScryptedUser:{name:"ScryptedUser",methods:["getScryptedUserAccessControl"],properties:[]},VideoFrameGenerator:{name:"VideoFrameGenerator",methods:["generateVideoFrames"],properties:[]},StreamService:{name:"StreamService",methods:["connectStream"],properties:[]},TTY:{name:"TTY",methods:[],properties:[]},TTYSettings:{name:"TTYSettings",methods:["getTTYSettings"],properties:[]},ScryptedSystemDevice:{name:"ScryptedSystemDevice",methods:[],properties:["systemDevice"]},ScryptedDeviceCreator:{name:"ScryptedDeviceCreator",methods:[],properties:[]},ScryptedSettings:{name:"ScryptedSettings",methods:[],properties:[]}},function(e){e.Builtin="Builtin",e.Internal="Internal",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.SmartDisplay="SmartDisplay",e.Speaker="Speaker",e.SmartSpeaker="SmartSpeaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.SecuritySystem="SecuritySystem",e.WindowCovering="WindowCovering",e.Siren="Siren",e.AirPurifier="AirPurifier",e.Internet="Internet",e.Network="Network",e.Bridge="Bridge",e.Unknown="Unknown"}(i||(t.ScryptedDeviceType=i={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(o||(t.HumidityMode=o={})),function(e){e.Auto="Auto",e.Manual="Manual"}(s||(t.FanMode=s={})),function(e){e.C="C",e.F="F"}(r||(t.TemperatureUnit=r={})),function(e){e.Off="Off",e.Cool="Cool",e.Heat="Heat",e.HeatCool="HeatCool",e.Auto="Auto",e.FanOnly="FanOnly",e.Purifier="Purifier",e.Eco="Eco",e.Dry="Dry",e.On="On"}(c||(t.ThermostatMode=c={})),function(e){e.Absolute="Absolute",e.Relative="Relative",e.Continuous="Continuous",e.Preset="Preset",e.Home="Home"}(d||(t.PanTiltZoomMovement=d={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(l||(t.LockState=l={})),function(e){e.Trickle="trickle",e.Charging="charging",e.NotCharging="not-charging"}(p||(t.ChargeState=p={})),function(e){e.Inactive="Inactive",e.Idle="Idle",e.Active="Active",e.ActiveNightMode="ActiveNightMode"}(m||(t.AirPurifierStatus=m={})),function(e){e.Manual="Manual",e.Automatic="Automatic"}(u||(t.AirPurifierMode=u={})),function(e){e.Unknown="Unknown",e.Excellent="Excellent",e.Good="Good",e.Fair="Fair",e.Inferior="Inferior",e.Poor="Poor"}(g||(t.AirQuality=g={})),function(e){e.Disarmed="Disarmed",e.HomeArmed="HomeArmed",e.AwayArmed="AwayArmed",e.NightArmed="NightArmed"}(b||(t.SecuritySystemMode=b={})),function(e){e.Sensor="Sensor",e.Occupied="Occupied",e.Time="Time",e.Error="Error"}(v||(t.SecuritySystemObstruction=v={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(h||(t.MediaPlayerState=h={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.ScryptedPluginRuntime="ScryptedPluginRuntime",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Buttons="Buttons",e.PressButtons="PressButtons",e.Sensors="Sensors",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.Microphone="Microphone",e.AudioVolumeControl="AudioVolumeControl",e.Display="Display",e.VideoCamera="VideoCamera",e.VideoCameraMask="VideoCameraMask",e.VideoTextOverlays="VideoTextOverlays",e.VideoRecorder="VideoRecorder",e.VideoRecorderManagement="VideoRecorderManagement",e.PanTiltZoom="PanTiltZoom",e.EventRecorder="EventRecorder",e.VideoClips="VideoClips",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Charger="Charger",e.Reboot="Reboot",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.BufferConverter="BufferConverter",e.MediaConverter="MediaConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.TamperSensor="TamperSensor",e.Sleep="Sleep",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.SecuritySystem="SecuritySystem",e.PM10Sensor="PM10Sensor",e.PM25Sensor="PM25Sensor",e.VOCSensor="VOCSensor",e.NOXSensor="NOXSensor",e.CO2Sensor="CO2Sensor",e.AirQualitySensor="AirQualitySensor",e.AirPurifier="AirPurifier",e.FilterMaintenance="FilterMaintenance",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ClusterForkInterface="ClusterForkInterface",e.ObjectTracker="ObjectTracker",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.ObjectDetectionPreview="ObjectDetectionPreview",e.ObjectDetectionGenerator="ObjectDetectionGenerator",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel",e.RTCSignalingClient="RTCSignalingClient",e.LauncherApplication="LauncherApplication",e.ScryptedUser="ScryptedUser",e.VideoFrameGenerator="VideoFrameGenerator",e.StreamService="StreamService",e.TTY="TTY",e.TTYSettings="TTYSettings",e.ScryptedSystemDevice="ScryptedSystemDevice",e.ScryptedDeviceCreator="ScryptedDeviceCreator",e.ScryptedSettings="ScryptedSettings"}(f||(t.ScryptedInterface=f={})),function(e){e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.ServerId="text/x-server-id",e.PushEndpoint="text/x-push-endpoint",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaStreamUrl="text/x-media-url",e.MediaObject="x-scrypted/x-scrypted-media-object",e.RequestMediaObject="x-scrypted/x-scrypted-request-media-object",e.RequestMediaStream="x-scrypted/x-scrypted-request-stream",e.MediaStreamFeedback="x-scrypted/x-media-stream-feedback",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.FFmpegTranscodeStream="x-scrypted/x-ffmpeg-transcode-stream",e.RTCSignalingChannel="x-scrypted/x-scrypted-rtc-signaling-channel",e.RTCSignalingSession="x-scrypted/x-scrypted-rtc-signaling-session",e.RTCConnectionManagement="x-scrypted/x-scrypted-rtc-connection-management",e.Image="x-scrypted/x-scrypted-image"}(y||(t.ScryptedMimeTypes=y={}))},927(e,t,n){"use strict";var a,i=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),o=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||(a=function(e){return a=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t},a(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n=a(e),s=0;s<n.length;s++)"default"!==n[s]&&i(t,e,n[s]);return o(t,e),t}),r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const c=s(n(562)),d=r(n(896)),l=r(n(928)),{systemManager:p,deviceManager:m,mediaManager:u}=c.default,g=new Set(["person","cat","dog","animal","bird","face","vehicle","car","truck","bus","motorcycle","bicycle","plate","package"]),b=["disabled","1 per minute","1 per 10 seconds","every detection"],v={disabled:1/0,"1 per minute":6e4,"1 per 10 seconds":1e4,"every detection":0};class h extends c.ScryptedDeviceBase{constructor(e){super(e),this.lastCapture=new Map,this.captures=new Map,this.listeners=[],this.imgDir=l.default.join(process.env.SCRYPTED_PLUGIN_VOLUME||"/tmp","detection-trainer-images");try{d.default.mkdirSync(this.imgDir,{recursive:!0})}catch{}this.loadState(),this.registerListeners()}loadState(){try{const e=this.storage.getItem("captures");if(e){const t=JSON.parse(e);for(const e of t)this.captures.set(e.id,e);this.console.log(`Loaded ${this.captures.size} captures from storage.`)}}catch(e){this.console.warn("Could not load captures from storage:",e)}for(const[e]of this.captures)try{this.storage.removeItem(`img:${e}`)}catch{}}saveCaptures(){try{this.storage.setItem("captures",JSON.stringify([...this.captures.values()]))}catch(e){this.console.warn("Could not save captures:",e)}}imgPath(e){return l.default.join(this.imgDir,`${e}.jpg`)}saveImage(e,t){try{d.default.writeFileSync(this.imgPath(e),t)}catch(t){this.console.warn(`Could not save image ${e}:`,t)}}loadImage(e){try{const t=this.imgPath(e);if(d.default.existsSync(t))return d.default.readFileSync(t)}catch{}}deleteCapture(e){try{d.default.unlinkSync(this.imgPath(e))}catch{}this.captures.delete(e),this.saveCaptures()}async getSettings(){const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector));let t;try{t=await c.default.endpointManager.getLocalEndpoint(void 0,{public:!0})}catch{t="/endpoint/scrypted-detection-trainer/public/"}const n=[{key:"info",title:"Detection Trainer",description:`${this.captures.size} captures stored (${[...this.captures.values()].filter(e=>!e.reviewed).length} pending review, ${[...this.captures.values()].filter(e=>e.reviewed&&"discard"!==e.label).length} labeled).`,readonly:!0,value:""},{key:"open_ui",title:"Review UI",description:"Open the detection review and labeling interface.",type:"html",readonly:!0,value:`<a href="${t}" target="_blank" style="display:inline-block;padding:8px 16px;background:#1a4d8a;color:#fff;border-radius:6px;text-decoration:none;font-size:13px;">Open Review UI β†—</a>`},{key:"autoCapture",title:"Auto-Capture",description:"Automatically capture detections in the background. Disable to use manual browsing only.",type:"boolean",value:this.storage.getItem("autoCapture")??"true"}];for(const t of e){const e=`rate:${t.id}`;n.push({key:e,title:t.name,group:"Capture Rate per Camera",description:"How often to capture detections from this camera.",value:this.storage.getItem(e)||"1 per minute",choices:[...b]})}return n}async putSetting(e,t){"open_ui"!==e&&"ui_link"!==e&&"info"!==e&&(this.storage.setItem(e,t),e.startsWith("rate:")&&this.registerListeners())}registerListeners(){for(const e of this.listeners)e();this.listeners=[];const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector));for(const t of e){const e=`rate:${t.id}`,n=this.storage.getItem(e)||"1 per minute";if("disabled"===n)continue;const a=t.listen(c.ScryptedInterface.ObjectDetector,async(e,a,i)=>{await this.onDetection(t.id,t.name,i,v[n])});this.listeners.push(()=>a.removeListener())}this.console.log(`Listening to ${this.listeners.length} camera(s).`)}async onDetection(e,t,n,a){if("false"===(this.storage.getItem("autoCapture")??"true"))return;if(!n?.detections?.length||!n.inputDimensions)return;const i=Date.now();if(i-(this.lastCapture.get(e)||0)<a)return;const o=n.detections.filter(e=>e.className&&g.has(e.className.toLowerCase())&&e.boundingBox);if(!o.length)return;const s=o.sort((e,t)=>(t.score||0)-(e.score||0))[0];if(this.captures.size>=2e3){const e=[...this.captures.values()].filter(e=>!e.reviewed).sort((e,t)=>e.timestamp-t.timestamp)[0];if(!e)return;this.deleteCapture(e.id)}let r;this.lastCapture.set(e,i);try{if(n.detectionId){const t=p.getDeviceById(e),a=await t.getDetectionInput(n.detectionId);r=await u.convertMediaObjectToBuffer(a,"image/jpeg")}}catch(e){this.console.warn(`Could not get detection image for ${t}:`,e)}if(!r)return;const c=`${i}-${Math.random().toString(36).slice(2,8)}`,d={id:c,cameraId:e,cameraName:t,timestamp:i,detectedClass:s.className,score:s.score||0,boundingBox:s.boundingBox,inputDimensions:n.inputDimensions,detectionId:n.detectionId,reviewed:!1};this.captures.set(c,d),this.saveImage(c,r),this.saveCaptures(),this.console.log(`Captured ${s.className} (${Math.round(100*(s.score||0))}%) from ${t} [${this.captures.size} total]`)}async onRequest(e,t){const n=new URL(e.url,"http://localhost").pathname.replace(e.rootPath,"");if("/api/browse-img"===n){const n=new URL(e.url,"http://localhost").searchParams,a=n.get("cameraId")?.replace(/[^a-zA-Z0-9_\-]/g,""),i=n.get("thumbnailId")?.replace(/[^a-zA-Z0-9_\-:.]/g,"");if(!a||!i)return t.send("Missing params",{code:400});try{const e=p.getDeviceById(a),n=await e.getVideoClipThumbnail(i),o=await u.convertMediaObjectToBuffer(n,"image/jpeg");return t.send(o,{headers:{"Content-Type":"image/jpeg","Cache-Control":"max-age=3600"}})}catch(e){return t.send("Image unavailable",{code:404})}}if(n.startsWith("/img/")){const e=n.slice(5).replace(/[^a-zA-Z0-9_\-]/g,""),a=this.loadImage(e);return a?t.send(a,{headers:{"Content-Type":"image/jpeg","Cache-Control":"max-age=3600"}}):t.send("Not found",{code:404})}if("/api/label"===n&&e.body){const n=e.body,a=JSON.parse("string"==typeof n?n:Buffer.isBuffer(n)?n.toString():String(n)),i=this.captures.get(a.id);return i?(i.label=a.label,i.reviewed=!0,"discard"===a.label?this.deleteCapture(a.id):(this.captures.set(a.id,i),this.saveCaptures()),t.send(JSON.stringify({ok:!0}),{headers:{"Content-Type":"application/json"}})):t.send("Not found",{code:404})}if("/api/cameras"===n){const e=Object.keys(p.getSystemState()).map(e=>p.getDeviceById(e)).filter(e=>e&&(e.type===c.ScryptedDeviceType.Camera||e.type===c.ScryptedDeviceType.Doorbell)&&e.interfaces?.includes(c.ScryptedInterface.ObjectDetector)).map(e=>({id:e.id,name:e.name}));return t.send(JSON.stringify(e),{headers:{"Content-Type":"application/json"}})}if("/api/browse"===n){const n=new URL(e.url,"http://localhost").searchParams,a=n.get("cameraId"),i=parseInt(n.get("hours")||"24");if(!a)return t.send("Missing cameraId",{code:400});try{const e=p.getDeviceById(a);if(!e)return t.send("Camera not found",{code:404});const n=Date.now(),o=n-3600*i*1e3,s=(await e.getVideoClips({startTime:o,endTime:n})||[]).filter(e=>e.detectionClasses?.length&&e.thumbnailId).slice(0,100).map(t=>({clipId:t.id,thumbnailId:t.thumbnailId,timestamp:t.startTime,detectionClasses:t.detectionClasses||[],boundingBox:null,inputDimensions:null,cameraId:a,cameraName:e.name}));return t.send(JSON.stringify(s),{headers:{"Content-Type":"application/json"}})}catch(e){return t.send(JSON.stringify({error:e.message}),{headers:{"Content-Type":"application/json"},code:500})}}if("/api/add-event"===n&&e.body){const n=e.body,a=JSON.parse("string"==typeof n?n:Buffer.isBuffer(n)?n.toString():String(n)),{cameraId:i,cameraName:o,thumbnailId:s,timestamp:r,detectedClass:c,boundingBox:d,inputDimensions:l,label:m}=a;if(!m||"discard"===m)return t.send(JSON.stringify({ok:!0}),{headers:{"Content-Type":"application/json"}});let g;try{const e=p.getDeviceById(i),t=await e.getVideoClipThumbnail(s);g=await u.convertMediaObjectToBuffer(t,"image/jpeg")}catch(e){this.console.warn("Could not get thumbnail for browse event:",e)}if(!g)return t.send(JSON.stringify({error:"Could not retrieve image"}),{headers:{"Content-Type":"application/json"},code:500});const b=`browse-${r}-${Math.random().toString(36).slice(2,6)}`,v={id:b,cameraId:i,cameraName:o,timestamp:r,detectedClass:c||"unknown",score:1,boundingBox:d||[0,0,l?.[0]||1920,l?.[1]||1080],inputDimensions:l||[1920,1080],reviewed:!0,label:m};return this.captures.set(b,v),this.saveImage(b,g),this.saveCaptures(),t.send(JSON.stringify({ok:!0,id:b}),{headers:{"Content-Type":"application/json"}})}if("/api/pending"===n){const e=[...this.captures.values()].filter(e=>!e.reviewed).sort((e,t)=>t.timestamp-e.timestamp).slice(0,50);return t.send(JSON.stringify(e),{headers:{"Content-Type":"application/json"}})}if("/api/labeled"===n){const n=parseInt(new URL(e.url,"http://localhost").searchParams.get("page")||"0"),a=50,i=[...this.captures.values()].filter(e=>e.reviewed).sort((e,t)=>t.timestamp-e.timestamp),o=i.slice(n*a,(n+1)*a);return t.send(JSON.stringify({items:o,total:i.length,page:n,pageSize:a}),{headers:{"Content-Type":"application/json"}})}if("/api/stats"===n){const e=[...this.captures.values()],n={total:e.length,pending:e.filter(e=>!e.reviewed).length,labeled:e.filter(e=>e.reviewed&&"discard"!==e.label).length,byLabel:{},byCamera:{},byDetectedClass:{}};for(const t of e)t.label&&(n.byLabel[t.label]=(n.byLabel[t.label]||0)+1),n.byCamera[t.cameraName]=(n.byCamera[t.cameraName]||0)+1,n.byDetectedClass[t.detectedClass]=(n.byDetectedClass[t.detectedClass]||0)+1;return t.send(JSON.stringify(n),{headers:{"Content-Type":"application/json"}})}if("/api/export"===n){const e=[...this.captures.values()].filter(e=>e.reviewed&&e.label&&"discard"!==e.label);if(!e.length)return t.send(JSON.stringify({error:"No labeled data yet"}),{headers:{"Content-Type":"application/json"},code:400});const n={person:0,animal:1,face:2,vehicle:3,plate:4,package:5,discard:-1},a=[];for(const t of e){const e=this.loadImage(t.id);if(!e)continue;const i=`${t.id}`;a.push({filename:`images/${i}.jpg`,content:e.toString("base64"),encoding:"base64"});const[o,s,r,c]=t.boundingBox,[d,l]=t.inputDimensions,p=(o+r/2)/d,m=(s+c/2)/l,u=r/d,g=c/l,b=`${n[t.label]} ${p.toFixed(6)} ${m.toFixed(6)} ${u.toFixed(6)} ${g.toFixed(6)}\n`;a.push({filename:`labels/${i}.txt`,content:b,encoding:"utf8"})}const i=["path: dataset","train: images","val: images","","nc: 6","names: ['person', 'animal', 'face', 'vehicle', 'plate', 'package']","","# Generated by Scrypted Detection Trainer",`# ${e.length} labeled samples`].join("\n");return a.push({filename:"data.yaml",content:i,encoding:"utf8"}),t.send(JSON.stringify({files:a,count:e.length}),{headers:{"Content-Type":"application/json"}})}if("/"===n||""===n||"/index.html"===n)return t.send(this.renderUI(),{headers:{"Content-Type":"text/html"}});t.send("Not found",{code:404})}renderUI(){return"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Detection Trainer</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js\"><\/script>\n<style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e8e8e8; min-height: 100vh; }\n header { background: #1a1a1a; border-bottom: 1px solid #333; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }\n header h1 { font-size: 18px; font-weight: 600; color: #fff; }\n .stats { display: flex; gap: 20px; font-size: 13px; color: #aaa; }\n .stat span { color: #fff; font-weight: 600; }\n .container { max-width: 1000px; margin: 0 auto; padding: 24px; }\n .card { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 12px; overflow: hidden; margin-bottom: 24px; }\n .card-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; display: flex; align-items: center; justify-content: space-between; }\n .card-header h2 { font-size: 15px; font-weight: 600; }\n .badge { background: #333; color: #aaa; font-size: 12px; padding: 2px 8px; border-radius: 20px; }\n .badge.orange { background: #3d2a00; color: #f90; }\n .badge.green { background: #0d2d0d; color: #4c4; }\n\n /* Detection card */\n .detection { display: grid; grid-template-columns: 420px 1fr; gap: 0; border-bottom: 1px solid #222; }\n .detection:last-child { border-bottom: none; }\n .detection-imgs { display: flex; gap: 8px; padding: 10px; background: #111; align-items: center; }\n .img-panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }\n .img-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: .5px; }\n .det-canvas { border-radius: 6px; display: block; }\n .det-class-badge { display: inline-block; background: #3d2a00; color: #f90; font-size: 11px; padding: 2px 8px; border-radius: 4px; }\n .detection-info { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }\n .detection-meta { font-size: 12px; color: #888; display: flex; flex-wrap: wrap; gap: 10px; }\n .detection-meta strong { color: #ccc; }\n .label-buttons { display: flex; flex-wrap: wrap; gap: 8px; }\n .label-btn { padding: 7px 14px; border-radius: 8px; border: 1px solid #444; background: #222; color: #ccc; cursor: pointer; font-size: 13px; transition: all .15s; }\n .label-btn:hover { border-color: #666; background: #2a2a2a; color: #fff; }\n .label-btn.person { border-color: #2a6; color: #4d9; }\n .label-btn.person:hover { background: #0d2a1a; }\n .label-btn.animal { border-color: #a63; color: #d85; }\n .label-btn.animal:hover { background: #2a1a0d; }\n .label-btn.face { border-color: #49c; color: #6be; }\n .label-btn.face:hover { background: #0d1a2a; }\n .label-btn.vehicle { border-color: #76b; color: #99d; }\n .label-btn.vehicle:hover { background: #1a1a2a; }\n .label-btn.discard { border-color: #622; color: #a44; }\n .label-btn.discard:hover { background: #2a0d0d; }\n .detection.labeled { opacity: 0.4; pointer-events: none; }\n .labeled-tag { font-size: 11px; color: #4d9; background: #0d2a1a; border: 1px solid #2a6; padding: 2px 8px; border-radius: 4px; }\n\n /* Empty state */\n .empty { padding: 48px; text-align: center; color: #555; }\n .empty .icon { font-size: 48px; margin-bottom: 12px; }\n\n /* Export section */\n .export-btn { padding: 10px 20px; background: #1a4d8a; border: none; border-radius: 8px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500; }\n .export-btn:hover { background: #1e5ca0; }\n .export-btn:disabled { background: #333; color: #666; cursor: not-allowed; }\n .export-info { font-size: 13px; color: #888; padding: 12px 20px; }\n\n /* Progress bar */\n .progress { height: 4px; background: #222; border-radius: 2px; overflow: hidden; margin-top: 8px; }\n .progress-bar { height: 100%; background: #1a6; border-radius: 2px; transition: width .3s; }\n\n .toast { position: fixed; bottom: 24px; right: 24px; background: #1a3; color: #fff; padding: 10px 18px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .3s; pointer-events: none; }\n .toast.show { opacity: 1; }\n\n .tab-bar { display: flex; gap: 2px; padding: 12px 20px 0; border-bottom: 1px solid #2a2a2a; }\n .tab { padding: 8px 14px; font-size: 13px; color: #888; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }\n .tab.active { color: #fff; border-bottom-color: #4a9; }\n .tab-content { padding: 20px; }\n .tab-panel { display: none; }\n .tab-panel.active { display: block; }\n\n .breakdown-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }\n .breakdown-item { background: #222; border-radius: 8px; padding: 12px; }\n .breakdown-item .label { font-size: 12px; color: #888; margin-bottom: 4px; }\n .breakdown-item .value { font-size: 20px; font-weight: 600; color: #fff; }\n\n /* Lightbox */\n .lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 1000; align-items: center; justify-content: center; flex-direction: column; gap: 12px; }\n .lightbox.open { display: flex; }\n .lightbox canvas { max-width: 92vw; max-height: 82vh; border-radius: 8px; cursor: zoom-out; }\n .lightbox-meta { color: #aaa; font-size: 13px; text-align: center; }\n .lightbox-close { position: absolute; top: 16px; right: 20px; font-size: 28px; color: #888; cursor: pointer; line-height: 1; }\n .lightbox-close:hover { color: #fff; }\n .det-canvas { cursor: zoom-in; }\n</style>\n</head>\n<body>\n<header>\n <h1>🎯 Detection Trainer</h1>\n <div class=\"stats\">\n <div>Pending <span id=\"stat-pending\">β€”</span></div>\n <div>Labeled <span id=\"stat-labeled\">β€”</span></div>\n <div>Total <span id=\"stat-total\">β€”</span></div>\n </div>\n</header>\n<div class=\"container\">\n\n <div class=\"card\">\n <div class=\"tab-bar\">\n <div class=\"tab active\" onclick=\"showTab('review')\">Review</div>\n <div class=\"tab\" onclick=\"showTab('browse')\">Browse Events</div>\n <div class=\"tab\" onclick=\"showTab('labeled')\">Labeled</div>\n <div class=\"tab\" onclick=\"showTab('stats')\">Stats</div>\n <div class=\"tab\" onclick=\"showTab('export')\">Export Dataset</div>\n </div>\n\n \x3c!-- Review tab --\x3e\n <div class=\"tab-panel active\" id=\"tab-review\">\n <div id=\"detections-list\"></div>\n </div>\n\n \x3c!-- Browse tab --\x3e\n <div class=\"tab-panel\" id=\"tab-browse\">\n <div class=\"tab-content\">\n <div style=\"display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px;\">\n <select id=\"browse-camera\" style=\"padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;\">\n <option value=\"\">Select camera…</option>\n </select>\n <select id=\"browse-hours\" style=\"padding:8px 12px;background:#222;border:1px solid #444;color:#fff;border-radius:6px;font-size:13px;\">\n <option value=\"1\">Last 1 hour</option>\n <option value=\"6\">Last 6 hours</option>\n <option value=\"24\" selected>Last 24 hours</option>\n <option value=\"72\">Last 3 days</option>\n </select>\n <button class=\"export-btn\" onclick=\"loadBrowse()\" style=\"padding:8px 16px;\">Load Events</button>\n <span id=\"browse-status\" style=\"font-size:13px;color:#888;\"></span>\n </div>\n <div id=\"browse-list\"></div>\n </div>\n </div>\n\n \x3c!-- Labeled tab --\x3e\n <div class=\"tab-panel\" id=\"tab-labeled\">\n <div id=\"labeled-list\"></div>\n </div>\n <div class=\"tab-panel\" id=\"tab-stats\">\n <div class=\"tab-content\">\n <p style=\"font-size:13px;color:#888;margin-bottom:16px;\">Breakdown of captured and labeled detections.</p>\n <h3 style=\"font-size:13px;color:#aaa;margin-bottom:10px;\">By Detected Class (what the model said)</h3>\n <div class=\"breakdown-grid\" id=\"stats-detected\"></div>\n <h3 style=\"font-size:13px;color:#aaa;margin:20px 0 10px;\">By Corrected Label (what you said)</h3>\n <div class=\"breakdown-grid\" id=\"stats-label\"></div>\n <h3 style=\"font-size:13px;color:#aaa;margin:20px 0 10px;\">By Camera</h3>\n <div class=\"breakdown-grid\" id=\"stats-camera\"></div>\n </div>\n </div>\n\n \x3c!-- Export tab --\x3e\n <div class=\"tab-panel\" id=\"tab-export\">\n <div class=\"tab-content\">\n <p style=\"font-size:13px;color:#888;margin-bottom:16px;\">\n Exports a YOLO-format dataset (images + labels + data.yaml) as a downloadable bundle.\n Only labeled detections are included. Review more detections first to build a larger dataset.\n </p>\n <div id=\"export-stats\" class=\"export-info\">Loading…</div>\n <div style=\"display:flex;gap:12px;align-items:center;margin-top:12px;\">\n <button class=\"export-btn\" id=\"export-btn\" onclick=\"exportDataset()\">Download Dataset</button>\n <span id=\"export-status\" style=\"font-size:13px;color:#888;\"></span>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<div class=\"lightbox\" id=\"lightbox\" onclick=\"closeLightbox()\">\n <div class=\"lightbox-close\" onclick=\"closeLightbox()\">βœ•</div>\n <canvas id=\"lightbox-canvas\"></canvas>\n <div class=\"lightbox-meta\" id=\"lightbox-meta\"></div>\n</div>\n\n<div class=\"toast\" id=\"toast\"></div>\n\n<script>\nconst BASE = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname;\nlet pending = [];\nlet labeledCount = 0;\n\nfunction imgError(img) {\n img.parentElement.innerHTML = '<div style=\"padding:20px;color:#555;font-size:12px;text-align:center\">No image</div>';\n}\n\nfunction drawDetection(img, r) {\n const [bx, by, bw, bh] = r.boundingBox;\n const [iw, ih] = r.inputDimensions;\n\n // --- Full frame canvas with box overlay ---\n const fullCanvas = document.getElementById('canvas-full-' + r.id);\n if (fullCanvas) {\n const cw = fullCanvas.width, ch = fullCanvas.height;\n const scale = Math.min(cw / iw, ch / ih);\n const dw = iw * scale, dh = ih * scale;\n const ox = (cw - dw) / 2, oy = (ch - dh) / 2;\n const ctx = fullCanvas.getContext('2d');\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, cw, ch);\n ctx.drawImage(img, ox, oy, dw, dh);\n // Draw bounding box\n const rx = ox + bx * scale, ry = oy + by * scale;\n const rw = bw * scale, rh = bh * scale;\n ctx.strokeStyle = '#f90';\n ctx.lineWidth = 2;\n ctx.strokeRect(rx, ry, rw, rh);\n // Label badge\n ctx.fillStyle = 'rgba(255,153,0,0.85)';\n ctx.fillRect(rx, ry - 18, rw, 18);\n ctx.fillStyle = '#000';\n ctx.font = 'bold 11px sans-serif';\n ctx.fillText(r.detectedClass + ' ' + Math.round(r.score * 100) + '%', rx + 3, ry - 4);\n }\n\n // --- Crop canvas ---\n const cropCanvas = document.getElementById('canvas-crop-' + r.id);\n if (cropCanvas) {\n const cc = cropCanvas.width, ch2 = cropCanvas.height;\n const ctx2 = cropCanvas.getContext('2d');\n ctx2.fillStyle = '#111';\n ctx2.fillRect(0, 0, cc, ch2);\n // Add padding around the crop\n const pad = Math.min(bw, bh) * 0.15;\n const sx = Math.max(0, bx - pad), sy = Math.max(0, by - pad);\n const sw = Math.min(iw - sx, bw + pad * 2), sh = Math.min(ih - sy, bh + pad * 2);\n const scale2 = Math.min(cc / sw, ch2 / sh);\n const dw2 = sw * scale2, dh2 = sh * scale2;\n const ox2 = (cc - dw2) / 2, oy2 = (ch2 - dh2) / 2;\n ctx2.drawImage(img, sx, sy, sw, sh, ox2, oy2, dw2, dh2);\n // Thin box outline on crop\n ctx2.strokeStyle = '#f90';\n ctx2.lineWidth = 1.5;\n const rx2 = ox2 + pad * scale2, ry2 = oy2 + pad * scale2;\n ctx2.strokeRect(rx2, ry2, bw * scale2, bh * scale2);\n }\n}\n\n// Cache loaded images so lightbox reuses them\nconst imgCache = new Map();\n\nfunction openLightbox(r) {\n const img = imgCache.get(r.id);\n if (!img) return;\n\n const lb = document.getElementById('lightbox');\n const lbCanvas = document.getElementById('lightbox-canvas');\n\n // Size canvas to image, capped at viewport\n const maxW = window.innerWidth * 0.9;\n const maxH = window.innerHeight * 0.8;\n const [iw, ih] = r.inputDimensions;\n const scale = Math.min(maxW / iw, maxH / ih, 1);\n lbCanvas.width = Math.round(iw * scale);\n lbCanvas.height = Math.round(ih * scale);\n\n const ctx = lbCanvas.getContext('2d');\n ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);\n\n // Draw all bounding boxes for this detection (primary + others in same event if available)\n const boxes = r.allDetections || [{ boundingBox: r.boundingBox, detectedClass: r.detectedClass, score: r.score }];\n const colors = ['#f90', '#4af', '#f44', '#4f4', '#f4f', '#ff4'];\n boxes.forEach((d, i) => {\n const [bx, by, bw, bh] = d.boundingBox;\n const color = colors[i % colors.length];\n const rx = bx * scale, ry = by * scale, rw = bw * scale, rh = bh * scale;\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.strokeRect(rx, ry, rw, rh);\n const label = d.detectedClass + ' ' + Math.round((d.score || 0) * 100) + '%';\n const textW = ctx.measureText(label).width + 8;\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.85;\n ctx.fillRect(rx, Math.max(0, ry - 20), textW, 20);\n ctx.globalAlpha = 1;\n ctx.fillStyle = '#000';\n ctx.font = 'bold 12px sans-serif';\n ctx.fillText(label, rx + 4, Math.max(14, ry - 4));\n });\n\n document.getElementById('lightbox-meta').textContent =\n r.cameraName + ' Β· ' + new Date(r.timestamp).toLocaleString() + ' Β· ' + iw + 'Γ—' + ih;\n lb.classList.add('open');\n document.addEventListener('keydown', lbKeyHandler);\n}\n\nfunction openLightboxImg(img, cameraName, timestamp) {\n const lb = document.getElementById('lightbox');\n const lbCanvas = document.getElementById('lightbox-canvas');\n const maxW = window.innerWidth * 0.9;\n const maxH = window.innerHeight * 0.8;\n const scale = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);\n lbCanvas.width = Math.round(img.naturalWidth * scale);\n lbCanvas.height = Math.round(img.naturalHeight * scale);\n const ctx = lbCanvas.getContext('2d');\n ctx.drawImage(img, 0, 0, lbCanvas.width, lbCanvas.height);\n document.getElementById('lightbox-meta').textContent =\n cameraName + ' Β· ' + new Date(timestamp).toLocaleString();\n lb.classList.add('open');\n document.addEventListener('keydown', lbKeyHandler);\n}\n\nfunction closeLightbox() {\n document.getElementById('lightbox').classList.remove('open');\n document.removeEventListener('keydown', lbKeyHandler);\n}\n\nfunction lbKeyHandler(e) {\n if (e.key === 'Escape') closeLightbox();\n}\n\nfunction showTab(name) {\n const names = ['review', 'browse', 'labeled', 'stats', 'export'];\n document.querySelectorAll('.tab').forEach((t, i) => {\n t.classList.toggle('active', names[i] === name);\n });\n document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));\n document.getElementById('tab-' + name).classList.add('active');\n if (name === 'stats') loadStats();\n if (name === 'export') loadExportInfo();\n if (name === 'labeled') loadLabeled(0);\n if (name === 'browse') initBrowse();\n}\n\nasync function initBrowse() {\n const sel = document.getElementById('browse-camera');\n if (sel.options.length > 1) return; // already loaded\n try {\n const res = await fetch(BASE + '/api/cameras');\n const cameras = await res.json();\n for (const cam of cameras) {\n const opt = document.createElement('option');\n opt.value = cam.id;\n opt.textContent = cam.name;\n sel.appendChild(opt);\n }\n if (cameras.length === 1) sel.value = cameras[0].id;\n } catch(e) {\n document.getElementById('browse-status').textContent = 'Error loading cameras';\n }\n}\n\nasync function loadBrowse() {\n const cameraId = document.getElementById('browse-camera').value;\n const hours = document.getElementById('browse-hours').value;\n const status = document.getElementById('browse-status');\n const list = document.getElementById('browse-list');\n\n if (!cameraId) { status.textContent = 'Select a camera first'; return; }\n\n status.textContent = 'Loading…';\n list.innerHTML = '';\n\n try {\n const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);\n const events = await res.json();\n\n if (events.error) { status.textContent = 'Error: ' + events.error; return; }\n if (!events.length) { status.textContent = 'No detection events found.'; list.innerHTML = '<div class=\"empty\"><div class=\"icon\">πŸ”</div><div>No ObjectDetector events in this time range.</div></div>'; return; }\n\n status.textContent = events.length + ' events found';\n\n list.innerHTML = events.map((ev, i) => {\n const date = new Date(ev.timestamp).toLocaleString();\n const dets = ev.detections || [];\n const primary = dets[0] || {};\n const score = Math.round((primary.score || 0) * 100);\n const allClasses = dets.map(d => d.className + ' ' + Math.round((d.score||0)*100) + '%').join(', ');\n return `\n <div class=\"detection\" id=\"bev-${i}\" style=\"opacity:1;transition:opacity .3s\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"bcanvas-${i}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\" id=\"bcrop-panel-${i}\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"bcanvas-crop-${i}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${ev.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${allClasses}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Add to dataset as:</div>\n <div class=\"label-buttons\" id=\"blabels-${i}\">\n <button class=\"label-btn person\" onclick=\"addEvent(${i})('person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"addEvent(${i})('animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"addEvent(${i})('face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"addEvent(${i})('vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"addEvent(${i})('plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"addEvent(${i})('package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"addEvent(${i})('discard')\">πŸ—‘ Skip</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Load images for each event\n for (let i = 0; i < events.length; i++) {\n const ev = events[i];\n loadBrowseImage(i, ev);\n }\n\n } catch(e) {\n status.textContent = 'Error: ' + e.message;\n }\n}\n\n// Store browse events for addEvent closure\nlet browseEvents = [];\n\nasync function loadBrowse() {\n const cameraId = document.getElementById('browse-camera').value;\n const hours = document.getElementById('browse-hours').value;\n const status = document.getElementById('browse-status');\n const list = document.getElementById('browse-list');\n\n if (!cameraId) { status.textContent = 'Select a camera first'; return; }\n\n status.textContent = 'Loading…';\n list.innerHTML = '';\n browseEvents = [];\n\n try {\n const res = await fetch(BASE + '/api/browse?cameraId=' + cameraId + '&hours=' + hours);\n const events = await res.json();\n\n if (events.error) { status.textContent = 'Error: ' + events.error; return; }\n if (!events.length) {\n status.textContent = 'No detection events found.';\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">πŸ”</div><div>No ObjectDetector events in this time range.</div></div>';\n return;\n }\n\n browseEvents = events;\n status.textContent = events.length + ' events';\n\n list.innerHTML = events.map((ev, i) => {\n const date = new Date(ev.timestamp).toLocaleString();\n const dets = ev.detections || [];\n const allClasses = [...new Set(dets.map(d => d.className))].join(', ');\n return `\n <div class=\"detection\" id=\"bev-${i}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"bcanvas-full-${i}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"bcanvas-crop-${i}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${ev.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${allClasses}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Add to dataset as:</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"addBrowseEvent(${i},'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"addBrowseEvent(${i},'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"addBrowseEvent(${i},'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"addBrowseEvent(${i},'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"addBrowseEvent(${i},'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"addBrowseEvent(${i},'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"addBrowseEvent(${i},'discard')\">πŸ—‘ Skip</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Load thumbnails for each event\n for (let i = 0; i < events.length; i++) {\n loadBrowseImage(i, events[i]);\n }\n\n } catch(e) {\n status.textContent = 'Error: ' + e.message;\n }\n}\n\nfunction loadBrowseImage(i, ev) {\n fetch(BASE + '/api/browse-img?cameraId=' + ev.cameraId + '&thumbnailId=' + encodeURIComponent(ev.thumbnailId))\n .then(r => r.ok ? r.blob() : null)\n .then(blob => {\n if (!blob) return;\n const url = URL.createObjectURL(blob);\n const img = new Image();\n img.onload = () => {\n imgCache.set('browse-' + i, img);\n const fullCanvas = document.getElementById('bcanvas-full-' + i);\n const cropCanvas = document.getElementById('bcanvas-crop-' + i);\n const iw = img.naturalWidth, ih = img.naturalHeight;\n // No bounding box for clip thumbnails β€” just draw the full image\n if (fullCanvas) {\n const ctx = fullCanvas.getContext('2d');\n const cw = fullCanvas.width, ch = fullCanvas.height;\n const scale = Math.min(cw / iw, ch / ih);\n const dw = iw * scale, dh = ih * scale;\n ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);\n ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);\n // Label classes\n const labels = (ev.detectionClasses || []).join(', ');\n ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);\n ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';\n ctx.fillText(labels, 4, ch-5);\n fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);\n }\n // Hide crop panel β€” no bounding box available\n const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel') as HTMLElement;\n if (cropPanel) cropPanel.style.display = 'none';\n URL.revokeObjectURL(url);\n };\n img.src = url;\n }).catch(() => {});\n}\n\nasync function addBrowseEvent(i, label) {\n const ev = browseEvents[i];\n if (!ev) return;\n const el = document.getElementById('bev-' + i);\n if (el) { el.style.opacity = '0.4'; el.querySelectorAll('button').forEach(b => b.disabled = true); }\n\n if (label !== 'discard') {\n try {\n const res = await fetch(BASE + '/api/add-event', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n cameraId: ev.cameraId,\n cameraName: ev.cameraName,\n thumbnailId: ev.thumbnailId,\n timestamp: ev.timestamp,\n detectedClass: (ev.detectionClasses || [])[0] || 'unknown',\n boundingBox: null,\n inputDimensions: null,\n label,\n }),\n });\n const data = await res.json();\n if (data.error) { toast('Error: ' + data.error, '#633'); if (el) el.style.opacity = '1'; el?.querySelectorAll('button').forEach(b => b.disabled = false); return; }\n toast('Added: ' + label, '#1a6');\n } catch(e) {\n toast('Failed: ' + e.message, '#633');\n if (el) el.style.opacity = '1';\n el?.querySelectorAll('button').forEach(b => b.disabled = false);\n return;\n }\n } else {\n toast('Skipped', '#555');\n }\n\n // Remove from list after short delay\n setTimeout(() => { if (el) el.remove(); }, 400);\n\n // Update stats\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n}\n\nconst LABEL_COLORS = { person:'#4d9', animal:'#d85', face:'#6be', vehicle:'#99d', plate:'#cc9', package:'#fc9', discard:'#a44' };\n\nasync function loadLabeled(page) {\n const list = document.getElementById('labeled-list');\n list.innerHTML = '<div class=\"empty\"><div style=\"color:#888\">Loading…</div></div>';\n try {\n const res = await fetch(BASE + '/api/labeled?page=' + page);\n const data = await res.json();\n const { items, total, pageSize } = data;\n const totalPages = Math.ceil(total / pageSize);\n\n if (!items.length) {\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">🏷️</div><div>No labeled detections yet.</div></div>';\n return;\n }\n\n const pagerHtml = totalPages > 1 ? `\n <div style=\"display:flex;align-items:center;gap:12px;padding:14px 16px;border-top:1px solid #222;font-size:13px;color:#888;\">\n ${page > 0 ? `<button class=\"label-btn\" onclick=\"loadLabeled(${page-1})\">← Prev</button>` : ''}\n <span>Page ${page+1} of ${totalPages} Β· ${total} total</span>\n ${page < totalPages-1 ? `<button class=\"label-btn\" onclick=\"loadLabeled(${page+1})\">Next β†’</button>` : ''}\n </div>` : '';\n\n list.innerHTML = items.map(r => {\n const date = new Date(r.timestamp).toLocaleString();\n const score = Math.round(r.score * 100);\n const labelColor = LABEL_COLORS[r.label] || '#aaa';\n return `\n <div class=\"detection\" id=\"ldet-${r.id}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"lcanvas-full-${r.id}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"lcanvas-crop-${r.id}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${r.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${r.detectedClass} ${score}%</div>\n <div style=\"display:inline-block;background:#1a1a1a;border:1px solid ${labelColor};color:${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;\">βœ“ ${r.label}</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">Change label:</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"relabel('${r.id}', 'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"relabel('${r.id}', 'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"relabel('${r.id}', 'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"relabel('${r.id}', 'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"relabel('${r.id}', 'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"relabel('${r.id}', 'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"relabel('${r.id}', 'discard')\">πŸ—‘ Discard</button>\n </div>\n </div>\n </div>`;\n }).join('') + pagerHtml;\n\n // Draw bounding boxes\n for (const r of items) {\n const img = new Image();\n img.onload = () => {\n imgCache.set(r.id, img);\n // Reuse drawDetection with the labeled canvases\n const origFull = document.getElementById('canvas-full-' + r.id);\n const origCrop = document.getElementById('canvas-crop-' + r.id);\n // Temporarily point to labeled canvases\n const fakeFull = document.getElementById('lcanvas-full-' + r.id);\n const fakeCrop = document.getElementById('lcanvas-crop-' + r.id);\n if (fakeFull) fakeFull.id = 'canvas-full-' + r.id;\n if (fakeCrop) fakeCrop.id = 'canvas-crop-' + r.id;\n drawDetection(img, r);\n if (fakeFull) fakeFull.id = 'lcanvas-full-' + r.id;\n if (fakeCrop) fakeCrop.id = 'lcanvas-crop-' + r.id;\n if (fakeFull) fakeFull.onclick = () => openLightbox(r);\n if (fakeCrop) fakeCrop.onclick = () => openLightbox(r);\n };\n img.src = BASE + '/img/' + r.id;\n }\n } catch(e) {\n list.innerHTML = '<div class=\"empty\"><div style=\"color:#a44\">Error: ' + e.message + '</div></div>';\n }\n}\n\nasync function relabel(id, labelVal) {\n await fetch(BASE + '/api/label', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, label: labelVal }),\n });\n const el = document.getElementById('ldet-' + id);\n if (labelVal === 'discard') {\n if (el) el.remove();\n toast('Discarded', '#633');\n } else {\n // Update the label badge in place\n const badge = el && el.querySelector('[style*=\"βœ“\"]');\n const labelColor = LABEL_COLORS[labelVal] || '#aaa';\n if (el) {\n const badges = el.querySelectorAll('.detection-meta > div');\n badges.forEach(b => { if (b.textContent.startsWith('βœ“')) b.remove(); });\n const meta = el.querySelector('.detection-meta');\n const newBadge = document.createElement('div');\n newBadge.style.cssText = `display:inline-block;background:#1a1a1a;border:1px solid ${labelColor};color:${labelColor};font-size:11px;padding:2px 8px;border-radius:4px;`;\n newBadge.textContent = 'βœ“ ' + labelVal;\n meta.appendChild(newBadge);\n }\n toast('Re-labeled: ' + labelVal, '#1a6');\n }\n // Refresh stats\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-pending').textContent = stats.pending;\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n}\n\nfunction toast(msg, color='#1a3') {\n const el = document.getElementById('toast');\n el.textContent = msg;\n el.style.background = color;\n el.classList.add('show');\n setTimeout(() => el.classList.remove('show'), 2500);\n}\n\nasync function loadPending() {\n try {\n const res = await fetch(BASE + '/api/pending');\n pending = await res.json();\n\n const statsRes = await fetch(BASE + '/api/stats');\n const stats = await statsRes.json();\n document.getElementById('stat-pending').textContent = stats.pending;\n document.getElementById('stat-labeled').textContent = stats.labeled;\n document.getElementById('stat-total').textContent = stats.total;\n\n const list = document.getElementById('detections-list');\n if (!pending.length) {\n list.innerHTML = '<div class=\"empty\"><div class=\"icon\">βœ…</div><div>No pending detections to review.<br><span style=\"font-size:12px;color:#444\">Captures will appear here as cameras detect objects.</span></div></div>';\n return;\n }\n\n list.innerHTML = pending.map(r => {\n const date = new Date(r.timestamp).toLocaleString();\n const score = Math.round(r.score * 100);\n const bb = r.boundingBox;\n const dim = r.inputDimensions;\n return `\n <div class=\"detection\" id=\"det-${r.id}\">\n <div class=\"detection-imgs\">\n <div class=\"img-panel\">\n <div class=\"img-label\">Full frame</div>\n <canvas id=\"canvas-full-${r.id}\" class=\"det-canvas\" width=\"240\" height=\"160\"></canvas>\n </div>\n <div class=\"img-panel\">\n <div class=\"img-label\">Crop</div>\n <canvas id=\"canvas-crop-${r.id}\" class=\"det-canvas\" width=\"160\" height=\"160\"></canvas>\n </div>\n </div>\n <div class=\"detection-info\">\n <div class=\"detection-meta\">\n <div><strong>${r.cameraName}</strong></div>\n <div>${date}</div>\n <div class=\"det-class-badge\">${r.detectedClass} ${score}%</div>\n </div>\n <div style=\"font-size:12px;color:#888;\">What is this actually?</div>\n <div class=\"label-buttons\">\n <button class=\"label-btn person\" onclick=\"label('${r.id}', 'person')\">πŸ‘€ Person</button>\n <button class=\"label-btn animal\" onclick=\"label('${r.id}', 'animal')\">🐾 Animal</button>\n <button class=\"label-btn face\" onclick=\"label('${r.id}', 'face')\">πŸ˜€ Face</button>\n <button class=\"label-btn vehicle\" onclick=\"label('${r.id}', 'vehicle')\">πŸš— Vehicle</button>\n <button class=\"label-btn\" onclick=\"label('${r.id}', 'plate')\">πŸ”’ Plate</button>\n <button class=\"label-btn\" onclick=\"label('${r.id}', 'package')\">πŸ“¦ Package</button>\n <button class=\"label-btn discard\" onclick=\"label('${r.id}', 'discard')\">πŸ—‘ Discard</button>\n </div>\n </div>\n </div>`;\n }).join('');\n\n // Draw bounding boxes and crops onto canvases after DOM is ready\n for (const r of pending) {\n const img = new Image();\n img.onload = () => {\n imgCache.set(r.id, img);\n drawDetection(img, r);\n // Wire up click to open lightbox\n const fullCanvas = document.getElementById('canvas-full-' + r.id);\n const cropCanvas = document.getElementById('canvas-crop-' + r.id);\n if (fullCanvas) fullCanvas.onclick = () => openLightbox(r);\n if (cropCanvas) cropCanvas.onclick = () => openLightbox(r);\n };\n img.onerror = () => {\n const c = document.getElementById('canvas-full-' + r.id);\n if (c) c.parentElement.innerHTML = '<div style=\"padding:10px;color:#555;font-size:11px\">No image</div>';\n };\n img.src = BASE + '/img/' + r.id;\n } } catch(e) {\n console.error('loadPending error', e);\n const list = document.getElementById('detections-list');\n if (list) list.innerHTML = '<div class=\"empty\"><div style=\"color:#a44\">Error loading captures: ' + e.message + '</div></div>';\n }\n}\n\nasync function label(id, labelVal) {\n const el = document.getElementById('det-' + id);\n if (el) {\n el.classList.add('labeled');\n const btns = el.querySelectorAll('.label-btn');\n btns.forEach(b => b.disabled = true);\n }\n await fetch(BASE + '/api/label', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id, label: labelVal }),\n });\n toast(labelVal === 'discard' ? 'Discarded' : 'Labeled: ' + labelVal, labelVal === 'discard' ? '#633' : '#1a6');\n setTimeout(() => {\n if (el) el.remove();\n loadPending();\n }, 600);\n}\n\nasync function loadStats() {\n const res = await fetch(BASE + '/api/stats');\n const stats = await res.json();\n\n const renderBreakdown = (obj, container) => {\n const el = document.getElementById(container);\n const entries = Object.entries(obj).sort((a, b) => b[1] - a[1]);\n el.innerHTML = entries.length\n ? entries.map(([k, v]) => `<div class=\"breakdown-item\"><div class=\"label\">${k}</div><div class=\"value\">${v}</div></div>`).join('')\n : '<div style=\"color:#555;font-size:13px;\">None yet</div>';\n };\n\n renderBreakdown(stats.byDetectedClass, 'stats-detected');\n renderBreakdown(stats.byLabel, 'stats-label');\n renderBreakdown(stats.byCamera, 'stats-camera');\n}\n\nasync function loadExportInfo() {\n const res = await fetch(BASE + '/api/stats');\n const stats = await res.json();\n document.getElementById('export-stats').textContent =\n `${stats.labeled} labeled samples ready for export across ${Object.keys(stats.byCamera).length} camera(s).`;\n}\n\nasync function exportDataset() {\n const btn = document.getElementById('export-btn');\n const status = document.getElementById('export-status');\n btn.disabled = true;\n status.textContent = 'Fetching data…';\n\n try {\n const res = await fetch(BASE + '/api/export');\n if (!res.ok) { status.textContent = 'Nothing to export yet.'; btn.disabled = false; return; }\n const data = await res.json();\n if (data.error) { status.textContent = data.error; btn.disabled = false; return; }\n\n status.textContent = 'Building zip…';\n\n const zip = new JSZip();\n for (const f of data.files) {\n if (f.encoding === 'base64') {\n zip.file(f.filename, f.content, { base64: true });\n } else {\n zip.file(f.filename, f.content);\n }\n }\n\n const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'scrypted_dataset_' + new Date().toISOString().slice(0,10) + '.zip';\n a.click();\n URL.revokeObjectURL(url);\n status.textContent = `Downloaded ${data.count} samples.`;\n toast('Dataset downloaded!');\n } catch (e) {\n status.textContent = 'Export failed: ' + e.message;\n }\n btn.disabled = false;\n}\n\n// Initial load\nloadPending();\n// Auto-refresh pending every 30s\nsetInterval(loadPending, 30_000);\n<\/script>\n</body>\n</html>"}}t.default=h},896(e){"use strict";e.exports=require("fs")},339(e){"use strict";e.exports=require("module")},928(e){"use strict";e.exports=require("path")}},t={};function n(a){var i=t[a];if(void 0!==i)return i.exports;var o=t[a]={exports:{}};return e[a].call(o.exports,o,o.exports,n),o.exports}n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var a=n(927),i=exports="undefined"==typeof exports?{}:exports;for(var o in a)i[o]=a[o];a.__esModule&&Object.defineProperty(i,"__esModule",{value:!0})})();
2
2
  //# sourceMappingURL=main.nodejs.js.map