This commit is contained in:
@@ -29,7 +29,10 @@
|
||||
<BarDropdownItem To="/setting/add_objects">Добавить объект</BarDropdownItem>
|
||||
</BarDropdownMenu>
|
||||
<BarDropdownMenu>
|
||||
<BarDropdownItem To="/simple-datagrid">Simple DataGrid</BarDropdownItem>
|
||||
<BarDropdownItem To="/setting/add_types">Добавить тип</BarDropdownItem>
|
||||
</BarDropdownMenu>
|
||||
<BarDropdownMenu>
|
||||
<BarDropdownItem To="/setting/add_devs">Добавить устройство</BarDropdownItem>
|
||||
</BarDropdownMenu>
|
||||
</BarDropdown>
|
||||
</BarItem>
|
||||
|
||||
@@ -6,6 +6,36 @@
|
||||
{
|
||||
<p>Название: @_obj.NameObject</p>
|
||||
<p>Количество устройств: @_devs.Count</p>
|
||||
|
||||
<Row>
|
||||
<Dropdown>
|
||||
<DropdownToggle Color="Color.Primary">
|
||||
Тип устройства
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
@foreach (var item in AllPubData.ListDeviceTypeItems)
|
||||
{
|
||||
<DropdownItem Clicked="@(() => ChangeType(item.Id))">@item.Name</DropdownItem>
|
||||
}
|
||||
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column ColumnSize="ColumnSize.Is2">
|
||||
<ListGroup>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
<ListGroupItem Color="Color.Dark">Dark</ListGroupItem>
|
||||
</ListGroup>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -15,13 +45,22 @@ else
|
||||
@code {
|
||||
[Parameter]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
|
||||
//public Guid IdType { get; set; }
|
||||
private DeviceTypeItems? _SelectedType;
|
||||
private ObjectItems? _obj;
|
||||
private List<DeviceItems> _devs = new();
|
||||
private List<DeviceItems> _show_devs = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_obj = AllPubData.ListObjectItems.FirstOrDefault(o => o.Id == Id);
|
||||
_devs = AllPubData.ListDeviceItems.Where(d => d.DeviceObject == Id).ToList();
|
||||
}
|
||||
|
||||
private void ChangeType(Guid id)
|
||||
{
|
||||
_SelectedType = AllPubData.ListDeviceTypeItems.First(t => t.Id == id);
|
||||
_show_devs = _devs.Where(d => d.DeviceType == id).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
@page "/setting/add_devs"
|
||||
@using Visor.Models
|
||||
|
||||
@* @inject HttpClient Http *@
|
||||
|
||||
<LoadingIndicator Visible="@isLoading" FullScreen />
|
||||
|
||||
|
||||
<Row Width="Width.Max100">
|
||||
<Column ColumnSize="ColumnSize.Is10"></Column>
|
||||
<Column ColumnSize="ColumnSize.Is2">
|
||||
<Button Color="Color.Primary" Clicked="@ShowAddObjetc">
|
||||
<Icon Name="IconName.Add" />
|
||||
</Button>
|
||||
<Button Color="Color.Primary" Clicked="@ShowAddObjetc">
|
||||
<Icon Name="IconName.FileUpload" />
|
||||
</Button>
|
||||
<Button Color="Color.Primary" Outline Clicked="@ShowDelObjetc">
|
||||
<Icon Name="IconName.Delete" />
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Divider />
|
||||
<Div>
|
||||
@if (isLoading || listObjects is null)
|
||||
{
|
||||
<Paragraph>@MessagePages</Paragraph>
|
||||
}
|
||||
else if (listObjects != null && listObjects.Any())
|
||||
{
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Название</TableHeaderCell>
|
||||
<TableHeaderCell>Описание</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var item in listObjects)
|
||||
{
|
||||
<TableRow>
|
||||
<TableRowCell>@item.NameObject</TableRowCell>
|
||||
<TableRowCell>@item.DescriptionObject</TableRowCell>
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Alert Color="Color.Warning">@MessagePages</Alert>
|
||||
<Paragraph>@MessagePages</Paragraph>
|
||||
}
|
||||
</Div>
|
||||
|
||||
|
||||
@code {
|
||||
private List<ObjectItems>? listObjects = AllPubData.ListObjectItems;
|
||||
private Modal modalAdd;
|
||||
private Modal modalDel;
|
||||
|
||||
private Guid select_id { get; set; }
|
||||
private int id_object { get; set; }
|
||||
private string name_object { get; set; }
|
||||
private string decript_object { get; set; }
|
||||
|
||||
private HttpClient Http = AllPubData.http;
|
||||
|
||||
|
||||
private string? MessagePages;
|
||||
bool isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
listObjects = AllPubData.ListObjectItems;
|
||||
if (listObjects.Count == 0)
|
||||
MessagePages = "Нет данных";
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
private Task ShowAddObjetc()
|
||||
{
|
||||
id_object = listObjects.Count;
|
||||
return modalAdd.Show();
|
||||
}
|
||||
private async Task AddObject()
|
||||
{
|
||||
var obj = new ObjectItems()
|
||||
{
|
||||
NameObject = name_object,
|
||||
DescriptionObject = decript_object
|
||||
};
|
||||
|
||||
var post = await Http.PostAsJsonAsync("api/Object/AddObject", obj);
|
||||
if (post.IsSuccessStatusCode)
|
||||
{
|
||||
listObjects.Add(obj);
|
||||
await modalAdd.Hide();
|
||||
//await AllPubData.ReloadObject();
|
||||
}
|
||||
|
||||
}
|
||||
private Task ShowDelObjetc() => modalDel.Show();
|
||||
private async Task DelObject()
|
||||
{
|
||||
var del = await Http.DeleteAsync($"api/Object/DelObject?id={select_id}");
|
||||
if (del.IsSuccessStatusCode)
|
||||
{
|
||||
listObjects.Remove(listObjects.Where(i => i.Id == select_id).First());
|
||||
await HideDelModal();
|
||||
}
|
||||
}
|
||||
private Task HideAddModal() => modalAdd.Hide();
|
||||
private Task HideDelModal() => modalDel.Hide();
|
||||
|
||||
}
|
||||
<Modal @ref="modalAdd" Centered>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Добавить объект</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
@* <Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Id</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<NumericInput Disabled Placeholder="Номер объекта" @bind-Value="@id_object" />
|
||||
</Addon>
|
||||
</Addons> *@
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Name</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<TextInput @bind-Value="@name_object" Placeholder="Имя объекта" />
|
||||
</Addon>
|
||||
</Addons>
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Описание</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<TextInput Placeholder="Дополнительная информация" @bind-Value="@decript_object" />
|
||||
</Addon>
|
||||
</Addons>
|
||||
<ModalFooter>
|
||||
<Button Color="Color.Secondary" Clicked="@HideAddModal">Close</Button>
|
||||
<Button Color="Color.Primary" Clicked="@AddObject">Добавить</Button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal @ref="modalDel" Centered>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Удалить объект</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Id</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<Select @bind-Value="@select_id">
|
||||
<SelectItem Value="9999">Объект</SelectItem>
|
||||
@foreach (var item in listObjects)
|
||||
{
|
||||
<SelectItem Value="@item.Id">@item.NameObject</SelectItem>
|
||||
}
|
||||
</Select>
|
||||
</Addon>
|
||||
</Addons>
|
||||
<ModalFooter>
|
||||
<Button Color="Color.Secondary" Clicked="@HideDelModal">Close</Button>
|
||||
<Button Color="Color.Primary" Clicked="@DelObject">Удалить</Button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
@@ -0,0 +1,179 @@
|
||||
@page "/setting/add_types"
|
||||
@using Visor.Models
|
||||
|
||||
@* @inject HttpClient Http *@
|
||||
|
||||
<LoadingIndicator Visible="@isLoading" FullScreen />
|
||||
|
||||
|
||||
<Row Width="Width.Max100">
|
||||
<Column ColumnSize="ColumnSize.Is10"></Column>
|
||||
<Column ColumnSize="ColumnSize.Is2">
|
||||
<Button Color="Color.Primary" Clicked="@ShowAddObjetc">
|
||||
<Icon Name="IconName.Add" />
|
||||
</Button>
|
||||
<Button Color="Color.Primary" Outline Clicked="@ShowDelObjetc">
|
||||
<Icon Name="IconName.Delete" />
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Divider />
|
||||
<Div>
|
||||
@if (isLoading || listObjects is null)
|
||||
{
|
||||
<Paragraph>@MessagePages</Paragraph>
|
||||
}
|
||||
else if (listObjects != null && listObjects.Any())
|
||||
{
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Название</TableHeaderCell>
|
||||
<TableHeaderCell>Описание</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var item in listObjects)
|
||||
{
|
||||
<TableRow>
|
||||
<TableRowCell>@item.NameObject</TableRowCell>
|
||||
<TableRowCell>@item.DescriptionObject</TableRowCell>
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Alert Color="Color.Warning">@MessagePages</Alert>
|
||||
<Paragraph>@MessagePages</Paragraph>
|
||||
}
|
||||
</Div>
|
||||
|
||||
|
||||
@code {
|
||||
private List<ObjectItems>? listObjects = AllPubData.ListObjectItems;
|
||||
private Modal modalAdd;
|
||||
private Modal modalDel;
|
||||
|
||||
private Guid select_id { get; set; }
|
||||
private int id_object { get; set; }
|
||||
private string name_object { get; set; }
|
||||
private string decript_object { get; set; }
|
||||
|
||||
private HttpClient Http = AllPubData.http;
|
||||
|
||||
|
||||
private string? MessagePages;
|
||||
bool isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await AllPubData.GetObjects();
|
||||
listObjects = AllPubData.ListObjectItems;
|
||||
if (listObjects.Count == 0)
|
||||
MessagePages = "Нет данных";
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
private Task ShowAddObjetc()
|
||||
{
|
||||
id_object = listObjects.Count;
|
||||
return modalAdd.Show();
|
||||
}
|
||||
private async Task AddObject()
|
||||
{
|
||||
var obj = new ObjectItems()
|
||||
{
|
||||
NameObject = name_object,
|
||||
DescriptionObject = decript_object
|
||||
};
|
||||
|
||||
var post = await Http.PostAsJsonAsync("api/Object/AddObject", obj);
|
||||
if (post.IsSuccessStatusCode)
|
||||
{
|
||||
listObjects.Add(obj);
|
||||
await modalAdd.Hide();
|
||||
//await AllPubData.ReloadObject();
|
||||
}
|
||||
|
||||
}
|
||||
private Task ShowDelObjetc() => modalDel.Show();
|
||||
private async Task DelObject()
|
||||
{
|
||||
var del = await Http.DeleteAsync($"api/Object/DelObject?id={select_id}");
|
||||
if (del.IsSuccessStatusCode)
|
||||
{
|
||||
listObjects.Remove(listObjects.Where(i => i.Id == select_id).First());
|
||||
await HideDelModal();
|
||||
}
|
||||
}
|
||||
private Task HideAddModal() => modalAdd.Hide();
|
||||
private Task HideDelModal() => modalDel.Hide();
|
||||
|
||||
}
|
||||
<Modal @ref="modalAdd" Centered>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Добавить объект</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
@* <Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Id</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<NumericInput Disabled Placeholder="Номер объекта" @bind-Value="@id_object" />
|
||||
</Addon>
|
||||
</Addons> *@
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Name</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<TextInput @bind-Value="@name_object" Placeholder="Имя объекта" />
|
||||
</Addon>
|
||||
</Addons>
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Описание</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<TextInput Placeholder="Дополнительная информация" @bind-Value="@decript_object" />
|
||||
</Addon>
|
||||
</Addons>
|
||||
<ModalFooter>
|
||||
<Button Color="Color.Secondary" Clicked="@HideAddModal">Close</Button>
|
||||
<Button Color="Color.Primary" Clicked="@AddObject">Добавить</Button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal @ref="modalDel" Centered>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Удалить объект</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Addons>
|
||||
<Addon AddonType="AddonType.Start">
|
||||
<AddonLabel>Id</AddonLabel>
|
||||
</Addon>
|
||||
<Addon AddonType="AddonType.Body">
|
||||
<Select @bind-Value="@select_id">
|
||||
<SelectItem Value="9999">Объект</SelectItem>
|
||||
@foreach (var item in listObjects)
|
||||
{
|
||||
<SelectItem Value="@item.Id">@item.NameObject</SelectItem>
|
||||
}
|
||||
</Select>
|
||||
</Addon>
|
||||
</Addons>
|
||||
<ModalFooter>
|
||||
<Button Color="Color.Secondary" Clicked="@HideDelModal">Close</Button>
|
||||
<Button Color="Color.Primary" Clicked="@DelObject">Удалить</Button>
|
||||
</ModalFooter>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
@@ -0,0 +1,597 @@
|
||||
import face_recognition
|
||||
import cv2
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import queue
|
||||
import base64
|
||||
import zipfile
|
||||
import json
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, render_template_string, Response, jsonify, send_from_directory, abort
|
||||
from flask_socketio import SocketIO, emit
|
||||
|
||||
app = Flask(__name__)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||
|
||||
# === КОНФИГУРАЦИЯ ===
|
||||
CONFIG_FILE = "config.json"
|
||||
DEFAULT_CONFIG = {
|
||||
"camera_ip": "192.168.1.100",
|
||||
"camera_stream": 1, # 1 - низкое качество, 2 - среднее, 0 - высокое
|
||||
"retention_days": 7,
|
||||
"process_interval": 0.2
|
||||
}
|
||||
|
||||
# === ЖЕСТКИЕ НАСТРОЙКИ КАМЕРЫ (СКРЫТЫ ОТ ОПЕРАТОРА) ===
|
||||
CAMERA_USER = "admin"
|
||||
CAMERA_PASS = "4NUDZhJ7"
|
||||
# Формат URL для Dahua/Hikvision. {stream} заменится на номер потока
|
||||
URL_TEMPLATE = "rtsp://{user}:{password}@{ip}/cam/realmonitor?channel=1&subtype={stream}"
|
||||
|
||||
def load_config():
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=4)
|
||||
return DEFAULT_CONFIG
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
config = load_config()
|
||||
|
||||
# Глобальные переменные
|
||||
known_face_encodings = []
|
||||
known_face_names = []
|
||||
frame_queue = queue.Queue(maxsize=2)
|
||||
last_processed_time = 0
|
||||
PROCESS_INTERVAL = float(config.get("process_interval", 0.2))
|
||||
|
||||
SAVE_DIR = "Лица"
|
||||
DATASET_DIR = "dataset"
|
||||
LOG_FILE = "access_log.csv"
|
||||
ARCHIVE_DIR = "logs_archive"
|
||||
RETENTION_DAYS = int(config.get("retention_days", 7))
|
||||
|
||||
last_sent_identity = None
|
||||
is_person_visible = False
|
||||
camera_thread_instance = None
|
||||
camera_stop_event = threading.Event()
|
||||
|
||||
# === ИНИЦИАЛИЗАЦИЯ ПАПОК ===
|
||||
for d in [SAVE_DIR, DATASET_DIR, ARCHIVE_DIR, os.path.join(SAVE_DIR, "Unknown")]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
if not os.path.exists(LOG_FILE):
|
||||
with open(LOG_FILE, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f, delimiter=';')
|
||||
writer.writerow(["Дата", "Время", "Имя", "Скриншот"])
|
||||
|
||||
def load_faces():
|
||||
global known_face_encodings, known_face_names
|
||||
print("Loading faces...")
|
||||
known_face_encodings = []
|
||||
known_face_names = []
|
||||
for filename in os.listdir(DATASET_DIR):
|
||||
if filename.endswith((".jpg", ".jpeg", ".png")):
|
||||
try:
|
||||
image = face_recognition.load_image_file(os.path.join(DATASET_DIR, filename))
|
||||
encodings = face_recognition.face_encodings(image)
|
||||
if len(encodings) > 0:
|
||||
known_face_encodings.append(encodings[0])
|
||||
known_face_names.append(os.path.splitext(filename)[0])
|
||||
except Exception as e:
|
||||
print(f"Error loading {filename}: {e}")
|
||||
print(f"✅ Loaded {len(known_face_names)} faces.")
|
||||
|
||||
load_faces()
|
||||
|
||||
def get_base64_image(path):
|
||||
try:
|
||||
with open(path, "rb") as img_file:
|
||||
return base64.b64encode(img_file.read()).decode('utf-8')
|
||||
except:
|
||||
return ""
|
||||
|
||||
def cleanup_old_files():
|
||||
now = time.time()
|
||||
cutoff = now - (RETENTION_DAYS * 86400)
|
||||
for root, dirs, files in os.walk(SAVE_DIR):
|
||||
for file in files:
|
||||
if file.endswith(".jpg"):
|
||||
filepath = os.path.join(root, file)
|
||||
if os.path.getmtime(filepath) < cutoff:
|
||||
try: os.remove(filepath)
|
||||
except: pass
|
||||
if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > 0:
|
||||
if os.path.getmtime(LOG_FILE) < cutoff:
|
||||
zip_name = os.path.join(ARCHIVE_DIR, f"log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip")
|
||||
try:
|
||||
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
zipf.write(LOG_FILE)
|
||||
open(LOG_FILE, 'w').close()
|
||||
except: pass
|
||||
|
||||
def write_log(name, screenshot_path):
|
||||
timestamp = datetime.now()
|
||||
with open(LOG_FILE, "a", newline='', encoding="utf-8") as f:
|
||||
writer = csv.writer(f, delimiter=';')
|
||||
writer.writerow([
|
||||
timestamp.strftime("%Y-%m-%d"),
|
||||
timestamp.strftime("%H:%M:%S"),
|
||||
name,
|
||||
screenshot_path
|
||||
])
|
||||
|
||||
def process_frame(frame):
|
||||
global last_processed_time, last_sent_identity, is_person_visible
|
||||
|
||||
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
|
||||
rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
face_locations = face_recognition.face_locations(rgb_small_frame)
|
||||
|
||||
if not face_locations:
|
||||
if is_person_visible:
|
||||
is_person_visible = False
|
||||
last_sent_identity = None
|
||||
return frame
|
||||
|
||||
top, right, bottom, left = face_locations[0]
|
||||
face_encoding = face_recognition.face_encodings(rgb_small_frame, [face_locations[0]])[0]
|
||||
|
||||
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
|
||||
name = "Unknown"
|
||||
if True in matches:
|
||||
name = known_face_names[matches.index(True)]
|
||||
|
||||
full_top, full_right = top * 4, right * 4
|
||||
full_bottom, full_left = bottom * 4, left * 4
|
||||
|
||||
should_notify = (name != last_sent_identity) or (not is_person_visible)
|
||||
|
||||
if should_notify:
|
||||
screenshot_path = ""
|
||||
b64_img = ""
|
||||
|
||||
pad_y, pad_x = 60, 40
|
||||
h, w = frame.shape[:2]
|
||||
crop = frame[max(0,full_top-pad_y):min(h,full_bottom+pad_y), max(0,full_left-pad_x):min(w,full_right+pad_x)]
|
||||
|
||||
if crop.size > 0:
|
||||
user_dir = os.path.join(SAVE_DIR, name)
|
||||
os.makedirs(user_dir, exist_ok=True)
|
||||
filename = f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.jpg"
|
||||
filepath = os.path.join(user_dir, filename)
|
||||
|
||||
cv2.imwrite(filepath, crop, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
|
||||
screenshot_path = filepath
|
||||
b64_img = get_base64_image(filepath)
|
||||
|
||||
write_log(name, filepath)
|
||||
threading.Thread(target=cleanup_old_files).start()
|
||||
|
||||
if name != "Unknown":
|
||||
if not b64_img:
|
||||
emp_photo_path = os.path.join(DATASET_DIR, f"{name}.jpg")
|
||||
b64_img = get_base64_image(emp_photo_path)
|
||||
socketio.emit('new_event', {'type': 'known', 'name': name, 'photo': b64_img, 'time': datetime.now().strftime("%H:%M:%S")})
|
||||
else:
|
||||
if b64_img:
|
||||
socketio.emit('new_event', {'type': 'unknown', 'name': 'НЕИЗВЕСТНЫЙ', 'photo': b64_img, 'path': screenshot_path, 'time': datetime.now().strftime("%H:%M:%S")})
|
||||
|
||||
last_sent_identity = name
|
||||
is_person_visible = True
|
||||
|
||||
color = (0, 255, 0) if name != "Unknown" else (0, 0, 255)
|
||||
cv2.rectangle(frame, (full_left, full_top), (full_right, full_bottom), color, 2)
|
||||
cv2.putText(frame, name, (full_left + 6, full_bottom - 6), cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), 1)
|
||||
return frame
|
||||
|
||||
def camera_thread(camera_url):
|
||||
print(f"🔌 Connecting to: {camera_url}")
|
||||
video_capture = cv2.VideoCapture(camera_url)
|
||||
video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
while not camera_stop_event.is_set():
|
||||
success, frame = video_capture.read()
|
||||
if not success:
|
||||
time.sleep(1)
|
||||
video_capture.release()
|
||||
video_capture = cv2.VideoCapture(camera_url)
|
||||
continue
|
||||
if frame_queue.full():
|
||||
try: frame_queue.get_nowait()
|
||||
except: pass
|
||||
frame_queue.put(frame)
|
||||
time.sleep(0.01)
|
||||
video_capture.release()
|
||||
|
||||
def start_camera():
|
||||
global camera_thread_instance, camera_stop_event
|
||||
if camera_thread_instance and camera_thread_instance.is_alive():
|
||||
camera_stop_event.set()
|
||||
camera_thread_instance.join(timeout=5)
|
||||
|
||||
camera_stop_event.clear()
|
||||
|
||||
ip = config.get("camera_ip", "")
|
||||
stream = config.get("camera_stream", 1)
|
||||
|
||||
if ip:
|
||||
# ИСПРАВЛЕНО: pass заменено на password
|
||||
url = URL_TEMPLATE.format(user=CAMERA_USER, password=CAMERA_PASS, ip=ip, stream=stream)
|
||||
camera_thread_instance = threading.Thread(target=camera_thread, args=(url,), daemon=True)
|
||||
camera_thread_instance.start()
|
||||
print(f"📷 Camera started for IP: {ip} (Stream: {stream})")
|
||||
else:
|
||||
print("⚠️ Camera IP not configured")
|
||||
|
||||
start_camera()
|
||||
|
||||
# === API ENDPOINTS ===
|
||||
|
||||
@app.route('/api/config', methods=['GET', 'POST'])
|
||||
def api_config():
|
||||
global config, PROCESS_INTERVAL, RETENTION_DAYS
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
|
||||
if 'camera_ip' in data: config['camera_ip'] = data['camera_ip']
|
||||
if 'camera_stream' in data: config['camera_stream'] = int(data['camera_stream'])
|
||||
|
||||
config['retention_days'] = int(data.get('retention_days', 7))
|
||||
config['process_interval'] = float(data.get('process_interval', 0.2))
|
||||
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
PROCESS_INTERVAL = config['process_interval']
|
||||
RETENTION_DAYS = config['retention_days']
|
||||
|
||||
# Перезапускаем камеру при изменении IP или потока
|
||||
if any(k in data for k in ['camera_ip', 'camera_stream']):
|
||||
start_camera()
|
||||
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
return jsonify(config)
|
||||
|
||||
@app.route('/api/log')
|
||||
def api_log():
|
||||
entries = []
|
||||
if os.path.exists(LOG_FILE):
|
||||
with open(LOG_FILE, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f, delimiter=';')
|
||||
next(reader, None)
|
||||
for row in reader:
|
||||
if len(row) >= 4:
|
||||
safe_path = row[3].replace("\\", "/")
|
||||
entries.append({
|
||||
'date': row[0], 'time': row[1],
|
||||
'name': row[2], 'path': safe_path
|
||||
})
|
||||
entries.reverse()
|
||||
return jsonify(entries)
|
||||
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
def api_upload():
|
||||
name = request.form.get('name', '').strip()
|
||||
photo = request.files.get('photo')
|
||||
if not name or not photo:
|
||||
return jsonify({"error": "Name and photo required"}), 400
|
||||
filename = f"{name.replace(' ', '_')}.jpg"
|
||||
path = os.path.join(DATASET_DIR, filename)
|
||||
photo.save(path)
|
||||
load_faces()
|
||||
return jsonify({"status": "ok", "name": name})
|
||||
|
||||
@app.route('/api/image/<path:filename>')
|
||||
def serve_image(filename):
|
||||
safe_dir = os.path.abspath(SAVE_DIR)
|
||||
requested_path = os.path.abspath(os.path.join(SAVE_DIR, filename))
|
||||
|
||||
if requested_path.startswith(safe_dir) and os.path.exists(requested_path):
|
||||
return send_from_directory(SAVE_DIR, filename)
|
||||
return abort(404)
|
||||
|
||||
@app.route('/video_feed')
|
||||
def video_feed():
|
||||
def generate():
|
||||
global last_processed_time
|
||||
while True:
|
||||
try:
|
||||
frame = frame_queue.get(timeout=5)
|
||||
if time.time() - last_processed_time >= PROCESS_INTERVAL:
|
||||
frame = process_frame(frame)
|
||||
last_processed_time = time.time()
|
||||
ret, buffer = cv2.imencode('.jpg', frame)
|
||||
yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
||||
except: continue
|
||||
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template_string('''
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Face Recognition System</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Segoe UI', sans-serif; background: #121212; color: white; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||
|
||||
.nav { display: flex; background: #1e1e1e; border-bottom: 1px solid #333; padding: 0 20px; }
|
||||
.nav-btn { padding: 15px 25px; cursor: pointer; color: #888; font-weight: 600; border-bottom: 3px solid transparent; transition: 0.3s; }
|
||||
.nav-btn:hover { color: white; }
|
||||
.nav-btn.active { color: #4CAF50; border-bottom-color: #4CAF50; }
|
||||
|
||||
.tab-content { display: none; flex: 1; overflow: hidden; }
|
||||
.tab-content.active { display: flex; }
|
||||
|
||||
.dashboard { display: flex; height: 100%; }
|
||||
.sidebar { width: 300px; background: #1e1e1e; padding: 15px; overflow-y: auto; border-right: 1px solid #333; display: flex; flex-direction: column; }
|
||||
.sidebar.right { border-right: none; border-left: 1px solid #333; }
|
||||
.main { flex: 1; padding: 20px; display: flex; align-items: center; justify-content: center; background: #000; position: relative; }
|
||||
|
||||
.card { background: #2c2c2c; padding: 12px; margin-bottom: 12px; border-radius: 10px; display: flex; align-items: center; cursor: pointer; transition: transform 0.2s; border-left: 4px solid #4CAF50; position: relative; }
|
||||
.card:hover { transform: translateX(5px); background: #333; }
|
||||
.card.alarm { border-left: 4px solid #ff4444; background: #3a2222; animation: pulse 1.5s infinite; }
|
||||
.card img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; margin-right: 15px; border: 2px solid #fff; }
|
||||
.card.alarm img { border-color: #ff4444; }
|
||||
|
||||
.delete-btn { position: absolute; top: 5px; right: 5px; width: 20px; height: 20px; background: rgba(0,0,0,0.5); color: #aaa; border-radius: 50%; text-align: center; line-height: 20px; font-size: 12px; cursor: pointer; display: none; z-index: 2; }
|
||||
.card:hover .delete-btn { display: block; }
|
||||
.delete-btn:hover { background: #ff4444; color: white; }
|
||||
|
||||
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255,68,68,0.4); } 70% { box-shadow: 0 0 0 10px rgba(255,68,68,0); } 100% { box-shadow: 0 0 0 0 rgba(255,68,68,0); } }
|
||||
|
||||
.col-header { text-align: center; font-size: 14px; font-weight: bold; padding: 10px 0; border-bottom: 1px solid #333; margin-bottom: 10px; position: sticky; top: 0; background: #1e1e1e; z-index: 10; }
|
||||
.col-known .col-header { color: #4CAF50; }
|
||||
.col-unknown .col-header { color: #ff4444; }
|
||||
|
||||
.header-bar { position: absolute; top: 20px; left: 20px; background: rgba(0,0,0,0.7); padding: 10px 20px; border-radius: 20px; display: flex; align-items: center; gap: 10px; backdrop-filter: blur(5px); z-index: 5; }
|
||||
.dot { width: 12px; height: 12px; border-radius: 50%; background: #4CAF50; box-shadow: 0 0 8px #4CAF50; transition: 0.3s; }
|
||||
.btn-sound { background: #333; border: 1px solid #555; color: #aaa; padding: 6px 12px; border-radius: 15px; cursor: pointer; font-size: 11px; margin-left: 10px; }
|
||||
|
||||
.name { font-weight: 600; font-size: 14px; }
|
||||
.time { font-size: 11px; color: #777; margin-top: 2px; }
|
||||
|
||||
.log-container { flex: 1; padding: 30px; overflow-y: auto; }
|
||||
.log-table { width: 100%; border-collapse: collapse; }
|
||||
.log-table th, .log-table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #333; }
|
||||
.log-table th { background: #1e1e1e; color: #888; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; position: sticky; top: 0; }
|
||||
.log-table tr:hover { background: #1e1e1e; }
|
||||
.log-table .thumb { width: 50px; height: 50px; border-radius: 8px; object-fit: cover; cursor: pointer; border: 1px solid #444; }
|
||||
.log-badge { padding: 3px 10px; border-radius: 10px; font-size: 11px; font-weight: bold; }
|
||||
.log-badge.known { background: rgba(76,175,80,0.2); color: #4CAF50; }
|
||||
.log-badge.unknown { background: rgba(255,68,68,0.2); color: #ff4444; }
|
||||
|
||||
.settings-container { flex: 1; padding: 40px; max-width: 700px; margin: 0 auto; overflow-y: auto; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; color: #888; font-size: 12px; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 1px; }
|
||||
.form-group input, .form-group select { width: 100%; padding: 12px 15px; background: #2c2c2c; border: 1px solid #444; border-radius: 8px; color: white; font-size: 14px; outline: none; transition: 0.3s; }
|
||||
.form-group input:focus, .form-group select:focus { border-color: #4CAF50; }
|
||||
.btn { padding: 12px 30px; background: #4CAF50; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: 0.3s; }
|
||||
.btn:hover { background: #43a047; }
|
||||
.btn-upload { background: #2196F3; margin-bottom: 20px; }
|
||||
.msg { margin-top: 10px; padding: 10px; border-radius: 8px; display: none; }
|
||||
.msg.ok { display: block; background: rgba(76,175,80,0.2); color: #4CAF50; }
|
||||
.msg.err { display: block; background: rgba(255,68,68,0.2); color: #ff4444; }
|
||||
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); justify-content: center; align-items: center; }
|
||||
.modal-content { max-width: 90%; max-height: 90%; border-radius: 8px; }
|
||||
.close { position: absolute; top: 20px; right: 30px; color: #f1f1f1; font-size: 40px; font-weight: bold; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nav">
|
||||
<div class="nav-btn active" onclick="switchTab('dashboard')">📹 Монитор</div>
|
||||
<div class="nav-btn" onclick="switchTab('log')">📋 Журнал</div>
|
||||
<div class="nav-btn" onclick="switchTab('settings')">⚙️ Настройки</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-dashboard" class="tab-content active dashboard">
|
||||
<div class="sidebar col-known">
|
||||
<div class="col-header">✅ СОТРУДНИКИ</div>
|
||||
<div id="known-list"></div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="header-bar">
|
||||
<div id="statusDot" class="dot"></div>
|
||||
<span id="statusText">LIVE</span>
|
||||
<button class="btn-sound" onclick="enableAudio()">🔊 Звук</button>
|
||||
</div>
|
||||
<img src="/video_feed" style="width:100%;height:100%;object-fit:contain;border-radius:8px;">
|
||||
</div>
|
||||
<div class="sidebar right col-unknown">
|
||||
<div class="col-header">⚠️ НЕИЗВЕСТНЫЕ</div>
|
||||
<div id="unknown-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-log" class="tab-content">
|
||||
<div class="log-container">
|
||||
<h2 style="margin-bottom:20px;color:#888;text-transform:uppercase;letter-spacing:1px;">Журнал посещений</h2>
|
||||
<table class="log-table">
|
||||
<thead><tr><th>Дата</th><th>Время</th><th>Имя</th><th>Статус</th><th>Фото</th></tr></thead>
|
||||
<tbody id="log-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<div class="settings-container">
|
||||
<h2 style="margin-bottom:30px;color:#888;text-transform:uppercase;letter-spacing:1px;">Настройки системы</h2>
|
||||
|
||||
<div style="background:#1e1e1e;padding:25px;border-radius:12px;margin-bottom:30px;">
|
||||
<h3 style="margin-bottom:15px;font-size:16px;">➕ Добавить сотрудника</h3>
|
||||
<form id="uploadForm" enctype="multipart/form-data">
|
||||
<div class="form-group"><label>Имя / Фамилия</label><input type="text" name="name" required placeholder="Иванов Иван"></div>
|
||||
<div class="form-group"><label>Фото лица</label><input type="file" name="photo" accept="image/*" required></div>
|
||||
<button type="submit" class="btn btn-upload">Загрузить</button>
|
||||
</form>
|
||||
<div id="uploadMsg" class="msg"></div>
|
||||
</div>
|
||||
|
||||
<div style="background:#1e1e1e;padding:25px;border-radius:12px;">
|
||||
<h3 style="margin-bottom:15px;font-size:16px;">📷 Подключение камеры</h3>
|
||||
<div class="form-group"><label>IP-адрес камеры</label><input type="text" id="cfg-ip" placeholder="192.168.1.100"></div>
|
||||
<div class="form-group">
|
||||
<label>Номер потока (качество)</label>
|
||||
<select id="cfg-stream">
|
||||
<option value="1">Поток 1 (Низкое качество / Быстро)</option>
|
||||
<option value="2">Поток 2 (Среднее качество)</option>
|
||||
<option value="0">Поток 0 (Высокое качество / HD)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:25px 0 15px;font-size:16px;">⚙️ Система</h3>
|
||||
<div class="form-group"><label>Хранить данные (дней)</label><input type="number" id="cfg-retention" min="1" max="365"></div>
|
||||
<div class="form-group"><label>Интервал обработки (сек)</label><input type="number" id="cfg-interval" step="0.1" min="0.1" max="2"></div>
|
||||
|
||||
<button class="btn" onclick="saveConfig()">💾 Сохранить и применить</button>
|
||||
<div id="configMsg" class="msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="imageModal" class="modal" onclick="closeModal()">
|
||||
<span class="close">×</span>
|
||||
<img class="modal-content" id="modalImg">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const socket = io();
|
||||
let audioEnabled = false;
|
||||
const alarmSound = new Audio('https://actions.google.com/sounds/v1/alarms/beep_short.ogg');
|
||||
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('tab-' + tab).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
if (tab === 'log') loadLog();
|
||||
if (tab === 'settings') loadConfig();
|
||||
}
|
||||
|
||||
function openModal(src) {
|
||||
document.getElementById('imageModal').style.display = 'flex';
|
||||
document.getElementById('modalImg').src = src;
|
||||
}
|
||||
function closeModal() { document.getElementById('imageModal').style.display = 'none'; }
|
||||
|
||||
function enableAudio() {
|
||||
audioEnabled = true;
|
||||
alarmSound.play().then(() => alarmSound.pause()).catch(()=>{});
|
||||
document.querySelector('.btn-sound').innerText = "🔊 Вкл";
|
||||
document.querySelector('.btn-sound').style.color = "#4CAF50";
|
||||
}
|
||||
|
||||
function removeCard(btn) {
|
||||
const card = btn.closest('.card');
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => card.remove(), 300);
|
||||
}
|
||||
|
||||
socket.on('new_event', (data) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = data.type === 'unknown' ? 'card alarm' : 'card';
|
||||
|
||||
let deleteBtn = '';
|
||||
if (data.type === 'unknown') {
|
||||
deleteBtn = `<div class="delete-btn" onclick="event.stopPropagation(); removeCard(this)">✕</div>`;
|
||||
}
|
||||
|
||||
div.onclick = () => openModal(`data:image/jpeg;base64,${data.photo}`);
|
||||
div.innerHTML = `${deleteBtn}<img src="data:image/jpeg;base64,${data.photo}"><div><div class="name">${data.name}</div><div class="time">${data.time}</div></div>`;
|
||||
|
||||
const list = data.type === 'unknown' ? document.getElementById('unknown-list') : document.getElementById('known-list');
|
||||
list.prepend(div);
|
||||
if (list.children.length > 20) list.lastChild.remove();
|
||||
|
||||
const dot = document.getElementById('statusDot');
|
||||
const st = document.getElementById('statusText');
|
||||
if (data.type === 'unknown') {
|
||||
dot.style.background = '#ff4444'; dot.style.boxShadow = '0 0 10px #ff4444';
|
||||
st.innerText = "ТРЕВОГА"; st.style.color = "#ff4444";
|
||||
if (audioEnabled) alarmSound.play();
|
||||
} else {
|
||||
dot.style.background = '#4CAF50'; dot.style.boxShadow = '0 0 8px #4CAF50';
|
||||
st.innerText = "LIVE"; st.style.color = "white";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const msg = document.getElementById('uploadMsg');
|
||||
try {
|
||||
const res = await fetch('/api/upload', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok) { msg.className = 'msg ok'; msg.textContent = `✅ ${data.name} добавлен!`; e.target.reset(); }
|
||||
else { msg.className = 'msg err'; msg.textContent = `❌ ${data.error}`; }
|
||||
} catch(err) { msg.className = 'msg err'; msg.textContent = '❌ Ошибка сети'; }
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await fetch('/api/config');
|
||||
const cfg = await res.json();
|
||||
|
||||
document.getElementById('cfg-ip').value = cfg.camera_ip || '';
|
||||
document.getElementById('cfg-stream').value = cfg.camera_stream || 1;
|
||||
document.getElementById('cfg-retention').value = cfg.retention_days || 7;
|
||||
document.getElementById('cfg-interval').value = cfg.process_interval || 0.2;
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const msg = document.getElementById('configMsg');
|
||||
const body = {
|
||||
camera_ip: document.getElementById('cfg-ip').value,
|
||||
camera_stream: document.getElementById('cfg-stream').value,
|
||||
retention_days: parseInt(document.getElementById('cfg-retention').value),
|
||||
process_interval: parseFloat(document.getElementById('cfg-interval').value)
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/config', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok) {
|
||||
msg.className = 'msg ok';
|
||||
msg.textContent = '✅ Настройки сохранены! Камера переподключается...';
|
||||
}
|
||||
else { msg.className = 'msg err'; msg.textContent = '❌ Ошибка сохранения'; }
|
||||
} catch(err) { msg.className = 'msg err'; msg.textContent = '❌ Ошибка сети'; }
|
||||
}
|
||||
|
||||
async function loadLog() {
|
||||
const res = await fetch('/api/log');
|
||||
const entries = await res.json();
|
||||
const tbody = document.getElementById('log-body');
|
||||
tbody.innerHTML = '';
|
||||
entries.forEach(e => {
|
||||
const isKnown = !e.name.includes('НЕИЗВЕСТНЫЙ') && !e.name.includes('Unknown');
|
||||
const badge = isKnown
|
||||
? '<span class="log-badge known">Сотрудник</span>'
|
||||
: '<span class="log-badge unknown">Неизвестный</span>';
|
||||
|
||||
const imgUrl = `/api/image/${e.path}`;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${e.date}</td>
|
||||
<td>${e.time}</td>
|
||||
<td style="font-weight:600">${e.name}</td>
|
||||
<td>${badge}</td>
|
||||
<td><img class="thumb" src="${imgUrl}" onerror="this.style.display='none'" onclick="openModal('${imgUrl}')" title="Нажмите для увеличения"></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
|
||||
@@ -4,6 +4,11 @@
|
||||
<Project Path="Strela/Strela.csproj" />
|
||||
<Project Path="Visor/Visor/Visor.csproj" Id="6216dfd5-6fb1-4085-a72a-0afec7ee86c2" />
|
||||
</Folder>
|
||||
<Folder Name="/LC/FaceRecognition/">
|
||||
<File Path="app.py" />
|
||||
</Folder>
|
||||
<Folder Name="/LC/FaceRecognition/dataset/" />
|
||||
<Folder Name="/LC/FaceRecognition/face-docker/" />
|
||||
<Folder Name="/Zabbix/">
|
||||
<Project Path="GenerateYAMLforZabbix/GenerateYAMLforZabbix.csproj" Id="b1c151d7-8be1-45e7-92f3-8420eb873b16" />
|
||||
<Project Path="getSwitch/getSwitch.csproj" Id="9e9bbe58-b5f9-4865-8565-2be53226e9cd" />
|
||||
|
||||
Reference in New Issue
Block a user