u
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ users.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
camera-settings.json
|
camera-settings.json
|
||||||
|
openai.key
|
||||||
47
README.md
47
README.md
@@ -162,7 +162,7 @@ curl -H "X-API-Key: your-secret-key" http://localhost:3080/stats/front-door
|
|||||||
GET /settings/:cameraId
|
GET /settings/:cameraId
|
||||||
```
|
```
|
||||||
|
|
||||||
Get v4l2 camera settings (focus, exposure, etc.) for a camera.
|
Get v4l2 camera settings (focus, exposure, etc.) for a camera. The camclient automatically registers available controls on first connection.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
@@ -173,13 +173,44 @@ curl -H "X-API-Key: your-secret-key" http://localhost:3080/settings/front-door
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"cameraId": "front-door",
|
"cameraId": "front-door",
|
||||||
"settings": {
|
"availableControls": [
|
||||||
"focus_automatic_continuous": 0,
|
{"name": "brightness", "type": "int", "min": -64, "max": 64, "step": 1, "default": 0},
|
||||||
"focus_absolute": 30,
|
{"name": "exposure_auto", "type": "menu", "min": 0, "max": 3, "default": 3, "options": {"1": "Manual Mode", "3": "Aperture Priority Mode"}}
|
||||||
"exposure_auto": 1,
|
],
|
||||||
"exposure_absolute": 200,
|
"values": {
|
||||||
|
"brightness": 10,
|
||||||
|
"focus_absolute": 40
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"rotation": null,
|
||||||
|
"crop": null,
|
||||||
|
"ocr": null,
|
||||||
|
"chartLabel": null,
|
||||||
|
"insertBrightnessToDb": false
|
||||||
|
},
|
||||||
|
"updatedAt": "2025-12-21T22:00:00.000Z",
|
||||||
|
"updatedBy": "webcam1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register Available Controls
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /settings/:cameraId/available
|
||||||
|
```
|
||||||
|
|
||||||
|
Called by the camclient to register all available v4l2 controls with their metadata. The camclient parses `v4l2-ctl -L` output and reports the schema.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"controls": [
|
||||||
|
{"name": "brightness", "type": "int", "min": -64, "max": 64, "step": 1, "default": 0},
|
||||||
|
{"name": "exposure_auto", "type": "menu", "options": {"1": "Manual", "3": "Auto"}, "default": 3}
|
||||||
|
],
|
||||||
|
"currentValues": {
|
||||||
"brightness": 0,
|
"brightness": 0,
|
||||||
"contrast": 32
|
"exposure_auto": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -190,7 +221,7 @@ curl -H "X-API-Key: your-secret-key" http://localhost:3080/settings/front-door
|
|||||||
PUT /settings/:cameraId
|
PUT /settings/:cameraId
|
||||||
```
|
```
|
||||||
|
|
||||||
Update camera settings. Settings are applied by the capture client via v4l2-ctl.
|
Update camera v4l2 control values or config settings (rotation, crop, ocr). Settings are applied by the capture client via v4l2-ctl.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -168,15 +168,136 @@ if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Discover available v4l2 controls and report them to server
|
||||||
|
discover_and_report_controls() {
|
||||||
|
if ! command -v v4l2-ctl &>/dev/null; then
|
||||||
|
log "v4l2-ctl not installed, skipping control discovery"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local AVAILABLE_URL="${API_URL%/upload}/settings/$CAMERA_ID/available"
|
||||||
|
|
||||||
|
log "Discovering available camera controls..."
|
||||||
|
|
||||||
|
# Parse v4l2-ctl -L output into JSON
|
||||||
|
# Example lines:
|
||||||
|
# brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0
|
||||||
|
# exposure_auto 0x009a0901 (menu) : min=0 max=3 default=3 value=3
|
||||||
|
# 1: Manual Mode
|
||||||
|
# 3: Aperture Priority Mode
|
||||||
|
|
||||||
|
local controls_json="["
|
||||||
|
local current_values_json="{"
|
||||||
|
local first_control=true
|
||||||
|
local first_value=true
|
||||||
|
local current_ctrl=""
|
||||||
|
local ctrl_json=""
|
||||||
|
local menu_options=""
|
||||||
|
local in_menu=false
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
# Check if this is a control line (starts with control name)
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*([a-z_]+)[[:space:]]+0x[0-9a-f]+[[:space:]]+\(([a-z]+)\)[[:space:]]*:[[:space:]]*(.*) ]]; then
|
||||||
|
# Save previous control if exists
|
||||||
|
if [[ -n "$current_ctrl" ]]; then
|
||||||
|
if [[ "$in_menu" == "true" && -n "$menu_options" ]]; then
|
||||||
|
ctrl_json="${ctrl_json%, }, \"options\": {${menu_options%,}}}"
|
||||||
|
else
|
||||||
|
ctrl_json="${ctrl_json%,}}"
|
||||||
|
fi
|
||||||
|
if [[ "$first_control" == "true" ]]; then
|
||||||
|
first_control=false
|
||||||
|
else
|
||||||
|
controls_json+=","
|
||||||
|
fi
|
||||||
|
controls_json+="$ctrl_json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_ctrl="${BASH_REMATCH[1]}"
|
||||||
|
local ctrl_type="${BASH_REMATCH[2]}"
|
||||||
|
local params="${BASH_REMATCH[3]}"
|
||||||
|
|
||||||
|
ctrl_json="{\"name\": \"$current_ctrl\", \"type\": \"$ctrl_type\","
|
||||||
|
menu_options=""
|
||||||
|
in_menu=false
|
||||||
|
|
||||||
|
# Parse parameters
|
||||||
|
if [[ "$params" =~ min=(-?[0-9]+) ]]; then
|
||||||
|
ctrl_json+=" \"min\": ${BASH_REMATCH[1]},"
|
||||||
|
fi
|
||||||
|
if [[ "$params" =~ max=(-?[0-9]+) ]]; then
|
||||||
|
ctrl_json+=" \"max\": ${BASH_REMATCH[1]},"
|
||||||
|
fi
|
||||||
|
if [[ "$params" =~ step=([0-9]+) ]]; then
|
||||||
|
ctrl_json+=" \"step\": ${BASH_REMATCH[1]},"
|
||||||
|
fi
|
||||||
|
if [[ "$params" =~ default=(-?[0-9]+) ]]; then
|
||||||
|
ctrl_json+=" \"default\": ${BASH_REMATCH[1]},"
|
||||||
|
fi
|
||||||
|
if [[ "$params" =~ value=(-?[0-9]+) ]]; then
|
||||||
|
local current_value="${BASH_REMATCH[1]}"
|
||||||
|
if [[ "$first_value" == "true" ]]; then
|
||||||
|
first_value=false
|
||||||
|
else
|
||||||
|
current_values_json+=","
|
||||||
|
fi
|
||||||
|
current_values_json+="\"$current_ctrl\": $current_value"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ctrl_type" == "menu" ]]; then
|
||||||
|
in_menu=true
|
||||||
|
fi
|
||||||
|
# Check if this is a menu option line (indented with number: description)
|
||||||
|
elif [[ "$in_menu" == "true" && "$line" =~ ^[[:space:]]+([0-9]+):[[:space:]]+(.+)$ ]]; then
|
||||||
|
local opt_val="${BASH_REMATCH[1]}"
|
||||||
|
local opt_name="${BASH_REMATCH[2]}"
|
||||||
|
menu_options+="\"$opt_val\": \"$opt_name\","
|
||||||
|
fi
|
||||||
|
done < <(v4l2-ctl -d "$VIDEO_DEVICE" -L 2>/dev/null)
|
||||||
|
|
||||||
|
# Save last control
|
||||||
|
if [[ -n "$current_ctrl" ]]; then
|
||||||
|
if [[ "$in_menu" == "true" && -n "$menu_options" ]]; then
|
||||||
|
ctrl_json="${ctrl_json%, }, \"options\": {${menu_options%,}}}"
|
||||||
|
else
|
||||||
|
ctrl_json="${ctrl_json%,}}"
|
||||||
|
fi
|
||||||
|
if [[ "$first_control" != "true" ]]; then
|
||||||
|
controls_json+=","
|
||||||
|
fi
|
||||||
|
controls_json+="$ctrl_json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
controls_json+="]"
|
||||||
|
current_values_json+="}"
|
||||||
|
|
||||||
|
# POST to server
|
||||||
|
local payload="{\"controls\": $controls_json, \"currentValues\": $current_values_json}"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -s --max-time 10 \
|
||||||
|
-X POST \
|
||||||
|
-H "X-API-Key: $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"$AVAILABLE_URL" 2>/dev/null) || true
|
||||||
|
|
||||||
|
if echo "$response" | grep -q '"success"'; then
|
||||||
|
local count
|
||||||
|
count=$(echo "$response" | grep -o '"controlsRegistered":[0-9]*' | cut -d: -f2)
|
||||||
|
log "Reported $count available controls to server"
|
||||||
|
else
|
||||||
|
log "Failed to report available controls: $response"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Fetch and apply camera settings from server
|
# Fetch and apply camera settings from server
|
||||||
apply_camera_settings() {
|
apply_camera_settings() {
|
||||||
# Check if v4l2-ctl is available
|
|
||||||
if ! command -v v4l2-ctl &>/dev/null; then
|
if ! command -v v4l2-ctl &>/dev/null; then
|
||||||
log "v4l2-ctl not installed, skipping camera settings"
|
log "v4l2-ctl not installed, skipping camera settings"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Derive settings URL from upload URL (replace /upload with /settings)
|
|
||||||
local SETTINGS_URL="${API_URL%/upload}/settings/$CAMERA_ID"
|
local SETTINGS_URL="${API_URL%/upload}/settings/$CAMERA_ID"
|
||||||
|
|
||||||
log "Fetching camera settings from server..."
|
log "Fetching camera settings from server..."
|
||||||
@@ -186,105 +307,67 @@ apply_camera_settings() {
|
|||||||
"$SETTINGS_URL" 2>/dev/null) || true
|
"$SETTINGS_URL" 2>/dev/null) || true
|
||||||
|
|
||||||
if [[ -z "$SETTINGS_RESPONSE" ]]; then
|
if [[ -z "$SETTINGS_RESPONSE" ]]; then
|
||||||
log "Could not fetch settings, using camera defaults"
|
log "Could not fetch settings, discovering and reporting controls..."
|
||||||
|
discover_and_report_controls
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if server has custom settings (look for updatedAt field)
|
# Check if server has available controls registered
|
||||||
local has_custom_settings=false
|
local has_controls=false
|
||||||
if command -v jq &>/dev/null; then
|
if command -v jq &>/dev/null; then
|
||||||
local updated_at
|
local ctrl_count
|
||||||
updated_at=$(echo "$SETTINGS_RESPONSE" | jq -r '.settings.updatedAt // empty')
|
ctrl_count=$(echo "$SETTINGS_RESPONSE" | jq -r '.availableControls | length // 0')
|
||||||
if [[ -n "$updated_at" && "$updated_at" != "null" ]]; then
|
if [[ "$ctrl_count" -gt 0 ]]; then
|
||||||
has_custom_settings=true
|
has_controls=true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if echo "$SETTINGS_RESPONSE" | grep -q '"updatedAt"'; then
|
if echo "$SETTINGS_RESPONSE" | grep -q '"availableControls":\[{'; then
|
||||||
has_custom_settings=true
|
has_controls=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$has_custom_settings" == "false" ]]; then
|
if [[ "$has_controls" == "false" ]]; then
|
||||||
# No custom settings on server - upload current camera settings
|
log "No controls registered, discovering and reporting..."
|
||||||
log "No settings on server, uploading current camera settings..."
|
discover_and_report_controls
|
||||||
upload_current_settings
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply settings from server
|
# Apply all values from server dynamically
|
||||||
if command -v jq &>/dev/null; then
|
if command -v jq &>/dev/null; then
|
||||||
local settings
|
local values
|
||||||
settings=$(echo "$SETTINGS_RESPONSE" | jq -r '.settings // empty')
|
values=$(echo "$SETTINGS_RESPONSE" | jq -r '.values // {}')
|
||||||
if [[ -n "$settings" ]]; then
|
local keys
|
||||||
for ctrl in focus_automatic_continuous focus_absolute exposure_auto exposure_absolute brightness contrast; do
|
keys=$(echo "$values" | jq -r 'keys[]' 2>/dev/null) || true
|
||||||
local value
|
|
||||||
value=$(echo "$settings" | jq -r ".$ctrl // empty")
|
local applied=0
|
||||||
if [[ -n "$value" && "$value" != "null" ]]; then
|
for ctrl in $keys; do
|
||||||
v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true
|
local value
|
||||||
|
value=$(echo "$values" | jq -r ".[\"$ctrl\"] // empty")
|
||||||
|
if [[ -n "$value" && "$value" != "null" ]]; then
|
||||||
|
if v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null; then
|
||||||
|
((applied++))
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "Applied $applied camera settings from server"
|
||||||
|
else
|
||||||
|
# Fallback: grep-based parsing for values
|
||||||
|
local values_block
|
||||||
|
values_block=$(echo "$SETTINGS_RESPONSE" | grep -o '"values":{[^}]*}' | sed 's/"values"://')
|
||||||
|
if [[ -n "$values_block" ]]; then
|
||||||
|
# Extract key:value pairs
|
||||||
|
while [[ "$values_block" =~ \"([a-z_]+)\":(-?[0-9]+) ]]; do
|
||||||
|
local ctrl="${BASH_REMATCH[1]}"
|
||||||
|
local value="${BASH_REMATCH[2]}"
|
||||||
|
v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true
|
||||||
|
values_block="${values_block#*${BASH_REMATCH[0]}}"
|
||||||
done
|
done
|
||||||
log "Applied camera settings from server"
|
log "Applied camera settings from server"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
for ctrl in focus_automatic_continuous focus_absolute exposure_auto exposure_absolute brightness contrast; do
|
|
||||||
local value
|
|
||||||
value=$(echo "$SETTINGS_RESPONSE" | grep -o "\"${ctrl}\":[0-9-]*" | cut -d: -f2)
|
|
||||||
if [[ -n "$value" ]]; then
|
|
||||||
v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
log "Applied camera settings from server"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read current camera settings and upload to server
|
# Only handle settings for fswebcam/v4l2
|
||||||
upload_current_settings() {
|
|
||||||
local SETTINGS_URL="${API_URL%/upload}/settings/$CAMERA_ID"
|
|
||||||
|
|
||||||
# Read current values from camera
|
|
||||||
local focus_auto focus_abs exp_auto exp_abs brightness contrast
|
|
||||||
focus_auto=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=focus_automatic_continuous 2>/dev/null | cut -d: -f2 | tr -d ' ') || focus_auto=""
|
|
||||||
focus_abs=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=focus_absolute 2>/dev/null | cut -d: -f2 | tr -d ' ') || focus_abs=""
|
|
||||||
exp_auto=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=exposure_auto 2>/dev/null | cut -d: -f2 | tr -d ' ') || exp_auto=""
|
|
||||||
exp_abs=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=exposure_absolute 2>/dev/null | cut -d: -f2 | tr -d ' ') || exp_abs=""
|
|
||||||
brightness=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=brightness 2>/dev/null | cut -d: -f2 | tr -d ' ') || brightness=""
|
|
||||||
contrast=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=contrast 2>/dev/null | cut -d: -f2 | tr -d ' ') || contrast=""
|
|
||||||
|
|
||||||
# Build JSON payload
|
|
||||||
local json_parts=()
|
|
||||||
[[ -n "$focus_auto" ]] && json_parts+=("\"focus_automatic_continuous\": $focus_auto")
|
|
||||||
[[ -n "$focus_abs" ]] && json_parts+=("\"focus_absolute\": $focus_abs")
|
|
||||||
[[ -n "$exp_auto" ]] && json_parts+=("\"exposure_auto\": $exp_auto")
|
|
||||||
[[ -n "$exp_abs" ]] && json_parts+=("\"exposure_absolute\": $exp_abs")
|
|
||||||
[[ -n "$brightness" ]] && json_parts+=("\"brightness\": $brightness")
|
|
||||||
[[ -n "$contrast" ]] && json_parts+=("\"contrast\": $contrast")
|
|
||||||
|
|
||||||
if [[ ${#json_parts[@]} -eq 0 ]]; then
|
|
||||||
log "Could not read camera settings"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Join with commas
|
|
||||||
local IFS=','
|
|
||||||
local json_body="{${json_parts[*]}}"
|
|
||||||
|
|
||||||
# Upload to server
|
|
||||||
local response
|
|
||||||
response=$(curl -s --max-time 10 \
|
|
||||||
-X PUT \
|
|
||||||
-H "X-API-Key: $API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$json_body" \
|
|
||||||
"$SETTINGS_URL" 2>/dev/null) || true
|
|
||||||
|
|
||||||
if echo "$response" | grep -q '"success"'; then
|
|
||||||
log "Uploaded current camera settings to server"
|
|
||||||
else
|
|
||||||
log "Failed to upload camera settings"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Only handle settings upload/apply for fswebcam/v4l2 for now
|
|
||||||
if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then
|
if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then
|
||||||
apply_camera_settings
|
apply_camera_settings
|
||||||
fi
|
fi
|
||||||
|
|||||||
35
filter_stats.js
Normal file
35
filter_stats.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const statsFile = 'uploads/rpi-webcam-2/2025/12/20/stats.json';
|
||||||
|
|
||||||
|
if (fs.existsSync(statsFile)) {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(statsFile, 'utf8');
|
||||||
|
const stats = JSON.parse(raw);
|
||||||
|
console.log(`Original count: ${stats.length}`);
|
||||||
|
|
||||||
|
const filtered = stats.filter(s => {
|
||||||
|
// Keep if ocr_val is missing (null/undefined) or >= 99
|
||||||
|
// User said: "filter values below 99".
|
||||||
|
// If ocr_val is null, it's not "below 99" technically, it's "no value".
|
||||||
|
// But if we are charting, we usually want to keep valid data.
|
||||||
|
// If ocr_val is strictly a number < 99, remove it.
|
||||||
|
if (typeof s.ocr_val === 'number') {
|
||||||
|
return s.ocr_val >= 99;
|
||||||
|
}
|
||||||
|
// Keep entries without ocr_val or null
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Filtered count: ${filtered.length}`);
|
||||||
|
|
||||||
|
fs.writeFileSync(statsFile, JSON.stringify(filtered, null, 2));
|
||||||
|
console.log('Stats file filtered and saved.');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error processing stats:', e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('File not found:', statsFile);
|
||||||
|
}
|
||||||
539
package-lock.json
generated
539
package-lock.json
generated
@@ -9,9 +9,13 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^12.5.0",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"ocrad.js": "^0.0.1",
|
||||||
|
"openai": "^6.15.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
"tesseract.js": "^7.0.0",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -515,6 +519,80 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "12.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
|
||||||
|
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bmp-js": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.4",
|
"version": "1.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
@@ -539,6 +617,30 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@@ -594,6 +696,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||||
@@ -660,6 +768,30 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -717,6 +849,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -762,6 +903,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.1",
|
"version": "4.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
@@ -808,6 +958,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
@@ -844,6 +1000,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -890,6 +1052,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -958,12 +1126,44 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/idb-keyval": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -973,6 +1173,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-url": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
@@ -1048,6 +1254,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
@@ -1069,6 +1287,12 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@@ -1094,6 +1318,12 @@
|
|||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@@ -1103,6 +1333,38 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.85.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
|
||||||
|
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -1124,6 +1386,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ocrad.js": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ocrad.js/-/ocrad.js-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-4W8Kcf4ewFJUgEGIBH4FxEgusHPmk0dSzD+CBMaaZeOb1RPM1Rv0+cA177pHDlN2yK8Gelb4KTczxtbQmq+p/w==",
|
||||||
|
"license": "GPL-3.0"
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -1136,6 +1404,45 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openai": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openai/-/openai-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/opencollective-postinstall": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"opencollective-postinstall": "index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1151,6 +1458,32 @@
|
|||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -1170,6 +1503,16 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
@@ -1209,6 +1552,21 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
@@ -1230,6 +1588,12 @@
|
|||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -1435,6 +1799,51 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -1467,6 +1876,81 @@
|
|||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tesseract.js": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bmp-js": "^0.1.0",
|
||||||
|
"idb-keyval": "^6.2.0",
|
||||||
|
"is-url": "^1.2.4",
|
||||||
|
"node-fetch": "^2.6.9",
|
||||||
|
"opencollective-postinstall": "^2.0.3",
|
||||||
|
"regenerator-runtime": "^0.13.3",
|
||||||
|
"tesseract.js-core": "^7.0.0",
|
||||||
|
"wasm-feature-detect": "^1.8.0",
|
||||||
|
"zlibjs": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tesseract.js-core": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
@@ -1476,6 +1960,12 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -1483,6 +1973,18 @@
|
|||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
@@ -1548,6 +2050,34 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wasm-feature-detect": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -1556,6 +2086,15 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zlibjs": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,13 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^12.5.0",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"ocrad.js": "^0.0.1",
|
||||||
|
"openai": "^6.15.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
"tesseract.js": "^7.0.0",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
<!-- Brightness Chart -->
|
<!-- Brightness Chart -->
|
||||||
<div id="chartContainer"
|
<div id="chartContainer"
|
||||||
style="margin-top: 40px; background: var(--card-bg); padding: 20px; border-radius: 8px; display: none;">
|
style="margin-top: 40px; background: var(--card-bg); padding: 20px; border-radius: 8px; display: none;">
|
||||||
<h3>Brightness Levels</h3>
|
<h3 id="chartTitle">Brightness Levels</h3>
|
||||||
<canvas id="brightnessChart" style="width: 100%; height: 200px;"></canvas>
|
<canvas id="brightnessChart" style="width: 100%; height: 200px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,8 @@
|
|||||||
const state = {
|
const state = {
|
||||||
cameras: [],
|
cameras: [],
|
||||||
selectedCamera: localStorage.getItem('lastCamera') || '',
|
selectedCamera: localStorage.getItem('lastCamera') || '',
|
||||||
selectedDate: localStorage.getItem('lastDate') || ''
|
selectedDate: localStorage.getItem('lastDate') || '',
|
||||||
|
currentSettings: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
@@ -258,6 +259,7 @@
|
|||||||
|
|
||||||
if (state.selectedCamera) {
|
if (state.selectedCamera) {
|
||||||
loadDates(state.selectedCamera);
|
loadDates(state.selectedCamera);
|
||||||
|
loadSettings(state.selectedCamera);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -299,6 +301,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSettings(cameraId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`settings/${cameraId}`, { headers: getHeaders() });
|
||||||
|
const data = await res.json();
|
||||||
|
state.currentSettings = data.settings || {};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load settings', err);
|
||||||
|
state.currentSettings = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadImages(isAutoUpdate = false) {
|
async function loadImages(isAutoUpdate = false) {
|
||||||
const cameraId = cameraSelect.value;
|
const cameraId = cameraSelect.value;
|
||||||
const dateStr = dateSelect.value; // YYYY-MM-DD
|
const dateStr = dateSelect.value; // YYYY-MM-DD
|
||||||
@@ -439,16 +452,49 @@
|
|||||||
|
|
||||||
const width = canvas.width;
|
const width = canvas.width;
|
||||||
const height = canvas.height;
|
const height = canvas.height;
|
||||||
const padding = { top: 20, right: 20, bottom: 30, left: 40 };
|
const padding = { top: 20, right: 30, bottom: 30, left: 45 };
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
// Sort stats by timestamp just in case
|
// Sort stats by timestamp just in case
|
||||||
stats.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
stats.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
||||||
|
|
||||||
|
// Determine data source and range
|
||||||
|
// If any entry has 'ocr_val', prefer that.
|
||||||
|
const hasOcr = stats.some(s => s.ocr_val !== undefined && s.ocr_val !== null);
|
||||||
|
const valueKey = hasOcr ? 'ocr_val' : 'brightness';
|
||||||
|
|
||||||
|
// Clean data (filter nulls)
|
||||||
|
const validPoints = stats
|
||||||
|
.filter(s => s[valueKey] !== undefined && s[valueKey] !== null)
|
||||||
|
.map(s => ({
|
||||||
|
t: s.timestamp,
|
||||||
|
v: Number(s[valueKey])
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (validPoints.length === 0) return;
|
||||||
|
|
||||||
|
let minY = Math.min(...validPoints.map(p => p.v));
|
||||||
|
let maxY = Math.max(...validPoints.map(p => p.v));
|
||||||
|
|
||||||
|
// Add some padding to Y range
|
||||||
|
const range = maxY - minY;
|
||||||
|
if (range === 0) {
|
||||||
|
minY -= 10;
|
||||||
|
maxY += 10;
|
||||||
|
} else {
|
||||||
|
minY -= range * 0.1;
|
||||||
|
maxY += range * 0.1;
|
||||||
|
}
|
||||||
|
// For brightness, clamp to 0-255 if not OCR?
|
||||||
|
if (!hasOcr) {
|
||||||
|
minY = 0;
|
||||||
|
maxY = 255;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to parsing time to minutes from start of day (local time)
|
// Helper to parsing time to minutes from start of day (local time)
|
||||||
const getMinutes = (iso) => {
|
const getMinutes = (iso) => {
|
||||||
// Parse UCT timestamp to Date object
|
// Parse UTC timestamp to Date object
|
||||||
const parts = iso.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
|
const parts = iso.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
|
||||||
let d;
|
let d;
|
||||||
if (parts) {
|
if (parts) {
|
||||||
@@ -460,14 +506,15 @@
|
|||||||
return d.getHours() * 60 + d.getMinutes();
|
return d.getHours() * 60 + d.getMinutes();
|
||||||
};
|
};
|
||||||
|
|
||||||
const points = stats.map(s => ({
|
const points = validPoints.map(p => ({
|
||||||
x: getMinutes(s.timestamp),
|
x: getMinutes(p.t),
|
||||||
y: s.brightness
|
y: p.v
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Scales
|
// Scales
|
||||||
const mapX = (minutes) => padding.left + (minutes / 1440) * (width - padding.left - padding.right);
|
const mapX = (minutes) => padding.left + (minutes / 1440) * (width - padding.left - padding.right);
|
||||||
const mapY = (val) => height - padding.bottom - (val / 255) * (height - padding.top - padding.bottom);
|
// Invert Y (0 at bottom)
|
||||||
|
const mapY = (val) => height - padding.bottom - ((val - minY) / (maxY - minY)) * (height - padding.top - padding.bottom);
|
||||||
|
|
||||||
// Draw Axes
|
// Draw Axes
|
||||||
ctx.strokeStyle = '#555';
|
ctx.strokeStyle = '#555';
|
||||||
@@ -486,7 +533,7 @@
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Draw Line
|
// Draw Line
|
||||||
ctx.strokeStyle = '#3b82f6';
|
ctx.strokeStyle = hasOcr ? '#10b981' : '#3b82f6'; // Green for OCR, Blue for Brightness
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
@@ -498,6 +545,14 @@
|
|||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw Dots
|
||||||
|
ctx.fillStyle = hasOcr ? '#10b981' : '#3b82f6';
|
||||||
|
for (const p of points) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mapX(p.x), mapY(p.y), 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
// Draw Labels (Time)
|
// Draw Labels (Time)
|
||||||
ctx.fillStyle = '#ccc';
|
ctx.fillStyle = '#ccc';
|
||||||
ctx.font = '12px sans-serif';
|
ctx.font = '12px sans-serif';
|
||||||
@@ -505,16 +560,35 @@
|
|||||||
|
|
||||||
for (let h = 0; h <= 24; h += 4) {
|
for (let h = 0; h <= 24; h += 4) {
|
||||||
const x = mapX(h * 60);
|
const x = mapX(h * 60);
|
||||||
ctx.fillText(`${h}:00`, x, height - 10);
|
if (x > padding.left) {
|
||||||
|
ctx.fillText(`${h}:00`, x, height - 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Labels (Brightness)
|
// Draw Labels (Value)
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
for (let b = 0; b <= 255; b += 64) {
|
// Draw 5 ticks
|
||||||
const y = mapY(b);
|
for (let i = 0; i <= 4; i++) {
|
||||||
ctx.fillText(Math.round(b), padding.left - 5, y);
|
const val = minY + (i / 4) * (maxY - minY);
|
||||||
|
const y = mapY(val);
|
||||||
|
ctx.fillText(Math.round(val), padding.left - 5, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(15, height / 2);
|
||||||
|
ctx.rotate(-Math.PI / 2);
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
const label = state.currentSettings.chartLabel || (hasOcr ? "Value" : "Brightness");
|
||||||
|
ctx.fillText(label, 0, 0);
|
||||||
|
|
||||||
|
// Update Header too
|
||||||
|
const titleEl = document.getElementById('chartTitle');
|
||||||
|
if (titleEl) titleEl.textContent = label;
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrettyTime(ts) {
|
function getPrettyTime(ts) {
|
||||||
@@ -541,8 +615,10 @@
|
|||||||
// Listeners
|
// Listeners
|
||||||
cameraSelect.addEventListener('change', (e) => {
|
cameraSelect.addEventListener('change', (e) => {
|
||||||
state.selectedCamera = e.target.value;
|
state.selectedCamera = e.target.value;
|
||||||
if (state.selectedCamera) loadDates(state.selectedCamera);
|
if (state.selectedCamera) {
|
||||||
else dateSelect.disabled = true;
|
loadDates(state.selectedCamera);
|
||||||
|
loadSettings(state.selectedCamera);
|
||||||
|
} else dateSelect.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
dateSelect.addEventListener('change', (e) => {
|
dateSelect.addEventListener('change', (e) => {
|
||||||
|
|||||||
370
server.js
370
server.js
@@ -4,6 +4,8 @@ import path from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
import { validateApiKey, loadUsers } from './users.js';
|
import { validateApiKey, loadUsers } from './users.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -51,30 +53,23 @@ function authenticate(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure multer storage
|
// Configure multer storage
|
||||||
|
const TEMP_UPLOAD_DIR = path.join(UPLOAD_DIR, 'temp');
|
||||||
|
|
||||||
|
// Ensure temp directory exists
|
||||||
|
if (!fs.existsSync(TEMP_UPLOAD_DIR)) {
|
||||||
|
fs.mkdirSync(TEMP_UPLOAD_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure multer storage - save to temp dir first
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const cameraId = req.body.cameraId || req.query.cameraId || 'default';
|
cb(null, TEMP_UPLOAD_DIR);
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// Create directory structure: uploads/{cameraId}/{YYYY}/{MM}/{DD}/
|
|
||||||
const year = now.getFullYear().toString();
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(now.getDate()).padStart(2, '0');
|
|
||||||
|
|
||||||
const destPath = path.join(UPLOAD_DIR, cameraId, year, month, day);
|
|
||||||
|
|
||||||
fs.mkdirSync(destPath, { recursive: true });
|
|
||||||
cb(null, destPath);
|
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const cameraId = req.body.cameraId || req.query.cameraId || 'default';
|
// Temporary filename: {timestamp}_{random}.{ext}
|
||||||
const now = new Date();
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||||
|
|
||||||
// Filename format: {cameraId}_{timestamp}.{ext}
|
|
||||||
const timestamp = now.toISOString().replace(/[:.]/g, '-');
|
|
||||||
const ext = path.extname(file.originalname) || '.jpg';
|
const ext = path.extname(file.originalname) || '.jpg';
|
||||||
|
cb(null, `${uniqueSuffix}${ext}`);
|
||||||
cb(null, `${cameraId}_${timestamp}${ext}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +109,66 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
|
|
||||||
const cameraId = req.body.cameraId || req.query.cameraId || 'default';
|
const cameraId = req.body.cameraId || req.query.cameraId || 'default';
|
||||||
|
|
||||||
|
// Move file from temp to final destination
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear().toString();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
|
const finalDir = path.join(UPLOAD_DIR, cameraId, year, month, day);
|
||||||
|
if (!fs.existsSync(finalDir)) {
|
||||||
|
fs.mkdirSync(finalDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = now.toISOString().replace(/[:.]/g, '-');
|
||||||
|
const ext = path.extname(req.file.originalname) || '.jpg';
|
||||||
|
const finalFilename = `${cameraId}_${timestamp}${ext}`;
|
||||||
|
const finalPath = path.join(finalDir, finalFilename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.renameSync(req.file.path, finalPath);
|
||||||
|
|
||||||
|
// Update req.file details for subsequent use
|
||||||
|
req.file.path = finalPath;
|
||||||
|
req.file.filename = finalFilename;
|
||||||
|
req.file.destination = finalDir;
|
||||||
|
} catch (moveError) {
|
||||||
|
console.error('Failed to move file to final destination:', moveError);
|
||||||
|
// Clean up temp file if move failed
|
||||||
|
try { fs.unlinkSync(req.file.path); } catch (e) { }
|
||||||
|
return res.status(500).json({ error: 'Failed to process upload storage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rotation and cropping if configured
|
||||||
|
try {
|
||||||
|
const user = req.authenticatedUser;
|
||||||
|
const storageKey = `${user}:${cameraId}`;
|
||||||
|
const allSettings = loadCameraSettings();
|
||||||
|
const settings = allSettings[storageKey] || {};
|
||||||
|
|
||||||
|
// Check if we need to modify the image
|
||||||
|
if (settings.rotation || settings.crop) {
|
||||||
|
console.log(`Applying transformations for ${cameraId}:`, { rotation: settings.rotation, crop: settings.crop });
|
||||||
|
let pipeline = sharp(req.file.path);
|
||||||
|
|
||||||
|
if (settings.rotation) {
|
||||||
|
pipeline = pipeline.rotate(settings.rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.crop) {
|
||||||
|
// crop format: { left, top, width, height }
|
||||||
|
pipeline = pipeline.extract(settings.crop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the original file with transformed version
|
||||||
|
const buffer = await pipeline.toBuffer();
|
||||||
|
fs.writeFileSync(req.file.path, buffer);
|
||||||
|
console.log(`Transformed image saved to ${req.file.path}`);
|
||||||
|
}
|
||||||
|
} catch (transformError) {
|
||||||
|
console.error('Failed to transform image:', transformError);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate thumbnail
|
// Generate thumbnail
|
||||||
try {
|
try {
|
||||||
const image = sharp(req.file.path);
|
const image = sharp(req.file.path);
|
||||||
@@ -124,6 +179,159 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
// Simple average of R, G, B mean values (0-255)
|
// Simple average of R, G, B mean values (0-255)
|
||||||
const brightness = Math.round((stats.channels[0].mean + stats.channels[1].mean + stats.channels[2].mean) / 3);
|
const brightness = Math.round((stats.channels[0].mean + stats.channels[1].mean + stats.channels[2].mean) / 3);
|
||||||
|
|
||||||
|
// Reload settings for OCR check
|
||||||
|
let ocr_val = null;
|
||||||
|
try {
|
||||||
|
const user = req.authenticatedUser;
|
||||||
|
const storageKey = `${user}:${cameraId}`;
|
||||||
|
const settings = loadCameraSettings()[storageKey] || {};
|
||||||
|
|
||||||
|
const ocrSettings = (typeof settings.ocr === 'object') ? settings.ocr : { enabled: !!settings.ocr };
|
||||||
|
|
||||||
|
if (ocrSettings.enabled) {
|
||||||
|
console.log(`Running OCR for ${cameraId} using OpenAI...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read API Key
|
||||||
|
const keyPath = path.join(__dirname, 'openai.key');
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
const apiKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
// console.log(`Debug: Key loaded`);
|
||||||
|
const openai = new OpenAI({ apiKey });
|
||||||
|
|
||||||
|
// Gather historical context for OCR
|
||||||
|
let last5Values = [];
|
||||||
|
let fiveDayMin = null;
|
||||||
|
let fiveDayMax = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cameraPath = path.join(UPLOAD_DIR, cameraId);
|
||||||
|
const now = new Date();
|
||||||
|
const allOcrVals = [];
|
||||||
|
|
||||||
|
// Check last 5 days
|
||||||
|
for (let daysAgo = 0; daysAgo < 5; daysAgo++) {
|
||||||
|
const d = new Date(now);
|
||||||
|
d.setDate(d.getDate() - daysAgo);
|
||||||
|
const y = d.getFullYear().toString();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0');
|
||||||
|
const statsPath = path.join(cameraPath, y, m, dd, 'stats.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(statsPath)) {
|
||||||
|
const dayStats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
||||||
|
const validVals = dayStats
|
||||||
|
.filter(e => e.ocr_val != null && typeof e.ocr_val === 'number')
|
||||||
|
.map(e => e.ocr_val);
|
||||||
|
allOcrVals.push(...validVals);
|
||||||
|
|
||||||
|
// Get last 5 from today only
|
||||||
|
if (daysAgo === 0 && validVals.length > 0) {
|
||||||
|
last5Values = validVals.slice(-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allOcrVals.length > 0) {
|
||||||
|
fiveDayMin = Math.min(...allOcrVals);
|
||||||
|
fiveDayMax = Math.max(...allOcrVals);
|
||||||
|
}
|
||||||
|
} catch (histErr) {
|
||||||
|
console.error('Error gathering OCR history:', histErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build context string
|
||||||
|
let contextStr = '';
|
||||||
|
if (last5Values.length > 0) {
|
||||||
|
contextStr += `Last ${last5Values.length} readings: ${last5Values.join(', ')}. `;
|
||||||
|
}
|
||||||
|
if (fiveDayMin !== null && fiveDayMax !== null) {
|
||||||
|
contextStr += `Expected range (last 5 days): ${fiveDayMin} to ${fiveDayMax}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptText = contextStr
|
||||||
|
? `Identify the numeric meter reading. ${contextStr}`
|
||||||
|
: `Identify the numeric meter reading.`;
|
||||||
|
|
||||||
|
// Prepare Image (Base64)
|
||||||
|
const imageBuffer = await sharp(req.file.path)
|
||||||
|
.resize({ width: 200, withoutEnlargement: true })
|
||||||
|
.toFormat('png')
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const base64Image = imageBuffer.toString('base64');
|
||||||
|
const dataUrl = `data:image/jpeg;base64,${base64Image}`;
|
||||||
|
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
reasoning_effort: "minimal",
|
||||||
|
model: "gpt-5-mini",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
"role": "user", "content": [
|
||||||
|
{ "type": "text", "text": promptText },
|
||||||
|
{ "type": "image_url", "image_url": { "url": dataUrl } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_format: {
|
||||||
|
"type": "json_schema", "json_schema": {
|
||||||
|
"name": "number_extraction", "strict": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "number": { "type": "number" } },
|
||||||
|
"required": ["number"], "additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = response.choices[0].message.content;
|
||||||
|
if (content) {
|
||||||
|
const result = JSON.parse(content);
|
||||||
|
let val = result.number;
|
||||||
|
|
||||||
|
if (ocrSettings.minval !== undefined && val < ocrSettings.minval) {
|
||||||
|
console.log(`OCR value ${val} skipped (below minval ${ocrSettings.minval})`);
|
||||||
|
val = null;
|
||||||
|
}
|
||||||
|
if (ocrSettings.maxval !== undefined && val > ocrSettings.maxval) {
|
||||||
|
console.log(`OCR value ${val} skipped (above maxval ${ocrSettings.maxval})`);
|
||||||
|
val = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val !== null) {
|
||||||
|
ocr_val = val;
|
||||||
|
console.log(`OpenAI OCR Result: ${ocr_val}`);
|
||||||
|
|
||||||
|
// Insert CO2 reading into ac_data.db
|
||||||
|
try {
|
||||||
|
const co2DbPath = '/home/seb/src/actest/ac_data.db';
|
||||||
|
if (fs.existsSync(co2DbPath)) {
|
||||||
|
const co2Db = new Database(co2DbPath);
|
||||||
|
const stmt = co2Db.prepare(`
|
||||||
|
INSERT INTO readings (dev_name, port, port_name, fan_speed)
|
||||||
|
VALUES ('Wall', 2, 'CO2', ?)
|
||||||
|
`);
|
||||||
|
stmt.run(ocr_val);
|
||||||
|
co2Db.close();
|
||||||
|
console.log(`CO2 reading ${ocr_val} ppm inserted into ac_data.db`);
|
||||||
|
}
|
||||||
|
} catch (dbErr) {
|
||||||
|
console.error('Failed to insert CO2 into ac_data.db:', dbErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('openai.key file not found.');
|
||||||
|
}
|
||||||
|
} catch (openaiError) {
|
||||||
|
console.error('OpenAI OCR Failed:', openaiError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ocrErr) {
|
||||||
|
console.error('OCR logic error:', ocrErr);
|
||||||
|
}
|
||||||
|
|
||||||
const thumbnailFilename = req.file.filename.replace(path.extname(req.file.filename), '_thumb.avif');
|
const thumbnailFilename = req.file.filename.replace(path.extname(req.file.filename), '_thumb.avif');
|
||||||
const thumbnailPath = path.join(path.dirname(req.file.path), thumbnailFilename);
|
const thumbnailPath = path.join(path.dirname(req.file.path), thumbnailFilename);
|
||||||
const dirPath = path.dirname(req.file.path);
|
const dirPath = path.dirname(req.file.path);
|
||||||
@@ -145,7 +353,8 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
dailyStats.push({
|
dailyStats.push({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
filename: req.file.filename,
|
filename: req.file.filename,
|
||||||
brightness
|
brightness,
|
||||||
|
ocr_val
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write back stats
|
// Write back stats
|
||||||
@@ -153,6 +362,29 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
fs.writeFileSync(statsFile, JSON.stringify(dailyStats, null, 2));
|
fs.writeFileSync(statsFile, JSON.stringify(dailyStats, null, 2));
|
||||||
} catch (e) { console.error('Error writing stats.json', e); }
|
} catch (e) { console.error('Error writing stats.json', e); }
|
||||||
|
|
||||||
|
// Insert brightness to ac_data.db if enabled
|
||||||
|
try {
|
||||||
|
const user = req.authenticatedUser;
|
||||||
|
const storageKey = `${user}:${cameraId}`;
|
||||||
|
const settings = loadCameraSettings()[storageKey] || {};
|
||||||
|
|
||||||
|
if (settings.insertBrightnessToDb) {
|
||||||
|
const dbPath = '/home/seb/src/actest/ac_data.db';
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO readings (dev_name, port, port_name, fan_speed)
|
||||||
|
VALUES ('Wall', 3, 'Light', ?)
|
||||||
|
`);
|
||||||
|
stmt.run(brightness);
|
||||||
|
db.close();
|
||||||
|
console.log(`Brightness ${brightness} inserted into ac_data.db as Light`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (dbErr) {
|
||||||
|
console.error('Failed to insert brightness into ac_data.db:', dbErr);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cameraId,
|
cameraId,
|
||||||
@@ -162,6 +394,7 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
thumbnailPath: thumbnailPath,
|
thumbnailPath: thumbnailPath,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
brightness,
|
brightness,
|
||||||
|
ocr_val,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -268,7 +501,6 @@ function saveCameraSettings(settings) {
|
|||||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get camera settings
|
|
||||||
// Get camera settings
|
// Get camera settings
|
||||||
app.get('/settings/:cameraId', authenticate, (req, res) => {
|
app.get('/settings/:cameraId', authenticate, (req, res) => {
|
||||||
const { cameraId } = req.params;
|
const { cameraId } = req.params;
|
||||||
@@ -276,23 +508,65 @@ app.get('/settings/:cameraId', authenticate, (req, res) => {
|
|||||||
const storageKey = `${user}:${cameraId}`;
|
const storageKey = `${user}:${cameraId}`;
|
||||||
|
|
||||||
const allSettings = loadCameraSettings();
|
const allSettings = loadCameraSettings();
|
||||||
const settings = allSettings[storageKey] || {};
|
const cameraConfig = allSettings[storageKey] || {};
|
||||||
|
|
||||||
|
// Return the dynamic schema with available controls and current values
|
||||||
res.json({
|
res.json({
|
||||||
cameraId,
|
cameraId,
|
||||||
settings: {
|
availableControls: cameraConfig.availableControls || [],
|
||||||
focus_automatic_continuous: settings.focus_automatic_continuous ?? 0,
|
values: cameraConfig.values || {},
|
||||||
focus_absolute: settings.focus_absolute ?? 30,
|
updatedAt: cameraConfig.updatedAt || null,
|
||||||
exposure_auto: settings.exposure_auto ?? 1,
|
updatedBy: cameraConfig.updatedBy || null,
|
||||||
exposure_absolute: settings.exposure_absolute ?? 200,
|
// Include any extra config like rotation, crop, ocr, etc.
|
||||||
brightness: settings.brightness ?? 0,
|
config: {
|
||||||
contrast: settings.contrast ?? 32,
|
rotation: cameraConfig.rotation,
|
||||||
...settings
|
crop: cameraConfig.crop,
|
||||||
|
ocr: cameraConfig.ocr,
|
||||||
|
chartLabel: cameraConfig.chartLabel,
|
||||||
|
insertBrightnessToDb: cameraConfig.insertBrightnessToDb
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update camera settings
|
// Register available camera controls (called by camclient on startup)
|
||||||
|
app.post('/settings/:cameraId/available', authenticate, express.json(), (req, res) => {
|
||||||
|
const { cameraId } = req.params;
|
||||||
|
const user = req.authenticatedUser;
|
||||||
|
const storageKey = `${user}:${cameraId}`;
|
||||||
|
const { controls, currentValues } = req.body;
|
||||||
|
|
||||||
|
if (!controls || !Array.isArray(controls)) {
|
||||||
|
return res.status(400).json({ error: 'controls array required in request body' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSettings = loadCameraSettings();
|
||||||
|
const existing = allSettings[storageKey] || {};
|
||||||
|
|
||||||
|
// Preserve existing configured values, but update available controls
|
||||||
|
allSettings[storageKey] = {
|
||||||
|
...existing,
|
||||||
|
availableControls: controls,
|
||||||
|
// Merge current camera values as defaults if no values configured yet
|
||||||
|
values: existing.values || currentValues || {},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
updatedBy: user
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveCameraSettings(allSettings);
|
||||||
|
console.log(`Registered ${controls.length} available controls for ${storageKey}`);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
cameraId,
|
||||||
|
controlsRegistered: controls.length,
|
||||||
|
values: allSettings[storageKey].values
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Failed to save available controls', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update camera settings (values)
|
||||||
app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
||||||
const { cameraId } = req.params;
|
const { cameraId } = req.params;
|
||||||
const user = req.authenticatedUser;
|
const user = req.authenticatedUser;
|
||||||
@@ -304,9 +578,28 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allSettings = loadCameraSettings();
|
const allSettings = loadCameraSettings();
|
||||||
|
const existing = allSettings[storageKey] || {};
|
||||||
|
|
||||||
|
// Separate v4l2 control values from config settings
|
||||||
|
const configKeys = ['rotation', 'crop', 'ocr', 'chartLabel', 'insertBrightnessToDb'];
|
||||||
|
const newValues = {};
|
||||||
|
const newConfig = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(newSettings)) {
|
||||||
|
if (configKeys.includes(key)) {
|
||||||
|
newConfig[key] = value;
|
||||||
|
} else {
|
||||||
|
newValues[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allSettings[storageKey] = {
|
allSettings[storageKey] = {
|
||||||
...(allSettings[storageKey] || {}),
|
...existing,
|
||||||
...newSettings,
|
...newConfig,
|
||||||
|
values: {
|
||||||
|
...(existing.values || {}),
|
||||||
|
...newValues
|
||||||
|
},
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
updatedBy: user
|
updatedBy: user
|
||||||
};
|
};
|
||||||
@@ -316,15 +609,20 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cameraId,
|
cameraId,
|
||||||
settings: allSettings[storageKey]
|
values: allSettings[storageKey].values,
|
||||||
|
config: {
|
||||||
|
rotation: allSettings[storageKey].rotation,
|
||||||
|
crop: allSettings[storageKey].crop,
|
||||||
|
ocr: allSettings[storageKey].ocr,
|
||||||
|
chartLabel: allSettings[storageKey].chartLabel,
|
||||||
|
insertBrightnessToDb: allSettings[storageKey].insertBrightnessToDb
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: 'Failed to save settings', details: error.message });
|
res.status(500).json({ error: 'Failed to save settings', details: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// === Web Interface Endpoints ===
|
// === Web Interface Endpoints ===
|
||||||
|
|
||||||
// Serve static files (frontend)
|
// Serve static files (frontend)
|
||||||
|
|||||||
Reference in New Issue
Block a user