Jump to content


Photo

[...I’m afraid I can’t do that] Domotica


  • Please log in to reply
3112 replies to this topic

#3101 harbinger

harbinger

    Rompicoglioni DOCG

  • Grandi donatori di sperma
  • PipPipPipPipPipPipPip
  • 49894 posts

Posted Yesterday, 10:28

Ho spostato l'analisi in locale sul vecchio macbook pro m1 (16/512) con LM Studio (Qwen3.5-4B-mlx 4bit) che espone un endpoint compatibile OpenAI. Invece di fare tutto il giro da LLM Vision passo quasi tutto da uno script in python. Nonostante l'aumento della latenza, dovuta a un più lento preprocessing, passare in locale mi costa solo 3 secondi in più per un doppio check con due temperature diverse. E Qwen3.5-4B, anche a 4 bit di quantizzazione, è decisamente più preciso di Seed-1.6 flash.

 

Script:

 

#!/usr/bin/env python3

"""
lm_studio_analyze.py v2 — Doppio check sicurezza via LM Studio locale
 
ARCHITETTURA: Tutte le mappe (nomi camera, contesto geometrico) sono
INTERNE allo script. Riceve solo parametri semplici senza spazi.
 
Uso:
  python3 lm_studio_analyze.py <snapshot_path> <device_id> <alarm_state> <periodo> <output_path>
 
Esempio:
  python3 lm_studio_analyze.py /config/www/tmp/sec_cam5_20260308.jpg \
    ds_7616ni_m21620230321ccrrl43839803wcvu_5 disarmed GIORNO \
    /config/www/tmp/llm_security_result.json
"""
import sys
import json
import base64
import logging
import time
import re
from pathlib import Path
 
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    import urllib.request
    import urllib.error
    HAS_REQUESTS = False
 
# ══════════════════════════════════════════════════════════════════
# CONFIGURAZIONE — modifica questi valori secondo il tuo setup
# ══════════════════════════════════════════════════════════════════
 
# IP LAN del MacBook (Tailscale non raggiungibile dal Core HAOS)
LM_STUDIO_URL = "http://...:1234"
 
# Identificativo del modello come appare in LM Studio
# (verifica con: curl http://...:1234/v1/models)
MODEL = "qwen3.5-4b"
 
TIMEOUT_SECONDS = 45
TEMPERATURES = [0.1, 0.3]
MAX_TOKENS = 200
 
# ══════════════════════════════════════════════════════════════════
# MAPPE TELECAMERE (spostate qui dallo YAML dell'automazione)
# ══════════════════════════════════════════════════════════════════
 
FRIENDLY_NAMES = {
    "ds_7616ni_m21620230321ccrrl43839803wcvu_1": "Cortile Nord",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_2": "Giardino Ovest",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_3": "Giardino Sud",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_4": "Cortile Ovest",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_5": "Cancelletto",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_6": "Cortile Sud",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_7": "Giardino Est",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_8": "Giardino Nord",
    "ds_7616ni_m21620230321ccrrl43839803wcvu_9": "Cortile Est",
}
 
CAMERA_CONTEXT_MAP = {
    "ds_7616ni_m21620230321ccrrl43839803wcvu_1": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_2": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_3": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_4": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_5": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_6": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_7": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_8": (
        "..."
        "..."
    ),
    "ds_7616ni_m21620230321ccrrl43839803wcvu_9": (
        "..."
        "..."
    ),
}
 
# ══════════════════════════════════════════════════════════════════
# JSON SCHEMA per structured output
# ══════════════════════════════════════════════════════════════════
 
DETECTION_SCHEMA = {
    "type": "object",
    "properties": {
        "presenza_umana": {
            "type": "string",
            "enum": ["SI", "NO"]
        },
        "tipo_soggetto": {
            "type": "string",
            "enum": ["sconosciuto", "postino", "familiare", "animale", "veicolo"]
        },
        "posizione": {
            "type": "string",
            "enum": ["strada", "cortile", "vicino_casa"]
        },
        "distanza": {
            "type": "string",
            "enum": ["vicino", "medio", "lontano"]
        },
        "livello_rischio": {
            "type": "string",
            "enum": ["VERDE", "GIALLO", "ROSSO"]
        },
        "azione_raccomandata": {
            "type": "string",
            "enum": ["ignora", "controlla", "intervieni"]
        },
        "descrizione_breve": {
            "type": "string",
            "maxLength": 65,
            "minLength": 1
        }
    },
    "required": [
        "presenza_umana", "tipo_soggetto", "posizione",
        "distanza", "livello_rischio", "azione_raccomandata",
        "descrizione_breve"
    ],
    "additionalProperties": False
}
 
DEFAULT_RESULT = {
    "presenza_umana": "NO",
    "livello_rischio": "GIALLO",
    "descrizione_breve": "Errore analisi",
    "tipo_soggetto": "sconosciuto",
    "posizione": "sconosciuta",
    "distanza": "medio",
    "azione_raccomandata": "controlla"
}
 
logging.basicConfig(
    level=logging.INFO,
    format="[SEC-AI] %(asctime)s %(levelname)s %(message)s",
    datefmt="%H:%M:%S",
    handlers=[
        logging.StreamHandler(),  # stderr (catturato da HA)
        logging.FileHandler("/config/www/tmp/lm_analyze.log", mode="a"),  # file persistente
    ]
)
log = logging.getLogger(__name__)
 
 
def encode_image_b64(image_path: str) -> str:
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")
 
 
def build_prompt(camera_friendly: str, alarm_state: str,
                 periodo: str, camera_context: str) -> str:
    return (
        f"Analisi sicurezza telecamera: {camera_friendly}\n"
        f"Stato allarme: {alarm_state} | Periodo: {periodo}\n"
        f"Geometria camera: {camera_context}\n\n"
 
        "ISTRUZIONI PASSO PER PASSO:\n"
        "1. Guarda l'immagine con attenzione.\n"
        "2. C'è una PERSONA visibile? (non ombre, non cespugli, non oggetti)\n"
        "   - Se NO → presenza_umana=NO, tipo_soggetto=animale, livello_rischio=VERDE\n"
        "   - Se SI → vai al passo 3\n"
        "3. C'è un VEICOLO visibile?\n"
        "   - Tesla bianca o Polo grigia → tipo_soggetto=veicolo, livello_rischio=VERDE\n"
        "   - Altro veicolo in cortile → tipo_soggetto=veicolo, livello_rischio=GIALLO\n"
        "4. Se c'è una PERSONA:\n"
        f"   - Allarme armed_away + persona in cortile → ROSSO\n"
        f"   - {periodo}=NOTTE + persona vicino casa → ROSSO\n"
        f"   - {periodo}=GIORNO + persona in zona privata → GIALLO\n"
        "   - Persona su strada pubblica → VERDE\n"
        "   - Postino/corriere → tipo_soggetto=postino, VERDE\n\n"
 
        "REGOLA DI COERENZA OBBLIGATORIA:\n"
        "- Se presenza_umana=NO e nessun veicolo sospetto → livello_rischio DEVE essere VERDE\n"
        "- Se presenza_umana=NO → tipo_soggetto NON può essere sconosciuto\n"
        "- descrizione_breve deve descrivere SOLO ciò che si VEDE realmente\n\n"
 
        "Rispondi SOLO con un JSON valido.\n"
        "ESEMPIO scena vuota di notte:\n"
        '{"presenza_umana":"NO","tipo_soggetto":"animale",'
        '"posizione":"cortile","distanza":"lontano",'
        '"livello_rischio":"VERDE","azione_raccomandata":"ignora",'
        '"descrizione_breve":"Nessuna presenza, scena notturna vuota"}\n\n'
 
        "ESEMPIO persona sospetta:\n"
        '{"presenza_umana":"SI","tipo_soggetto":"sconosciuto",'
        '"posizione":"vicino_casa","distanza":"vicino",'
        '"livello_rischio":"ROSSO","azione_raccomandata":"intervieni",'
        '"descrizione_breve":"Persona sconosciuta vicino alla porta"}\n\n'
 
        "Analizza l'immagine. JSON:"
    )
 
 
def call_lm_studio(image_b64: str, prompt: str, temperature: float) -> dict | None:
    """Chiama LM Studio con visione. NO response_format (causa <|im_end|> leak)."""
    url = f"{LM_STUDIO_URL}/v1/chat/completions"
 
    payload = {
        "model": MODEL,
        "messages": [
            {
                "role": "system",
                "content": (
                    "Sei un analizzatore di sicurezza. "
                    "Rispondi ESCLUSIVAMENTE con un singolo oggetto JSON valido. "
                    "Nessun testo prima o dopo il JSON. Nessun commento. "
                    "Nessun markdown. Solo JSON puro."
                )
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{image_b64}"
                        }
                    },
                    {
                        "type": "text",
                        "text": prompt
                    }
                ]
            }
        ],
        "temperature": temperature,
        "max_tokens": MAX_TOKENS,
        "stream": False
        # NO response_format — causa leak di <|im_end|> con VLM in LM Studio
    }
 
    headers = {"Content-Type": "application/json"}
 
    try:
        log.info(f"  POST {url} (T={temperature}, model={MODEL})")
 
        if HAS_REQUESTS:
            resp = requests.post(url, json=payload, headers=headers,
                                 timeout=TIMEOUT_SECONDS)
            log.info(f"  HTTP {resp.status_code}")
            resp.raise_for_status()
            data = resp.json()
        else:
            body = json.dumps(payload).encode("utf-8")
            req = urllib.request.Request(url, data=body, headers=headers,
                                         method="POST")
            with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as response:
                log.info(f"  HTTP {response.status}")
                data = json.loads(response.read().decode("utf-8"))
 
        content = data["choices"][0]["message"]["content"]
        log.info(f"  Raw (first 200): {repr(content[:200])}")
 
        # ── Pulizia aggressiva dei token speciali Qwen ──
        # Rimuovi tutti i token di controllo del chat template
        content = re.sub(r'<\|im_start\|>[^\n]*\n?', '', content)
        content = re.sub(r'<\|im_end\|>', '', content)
        content = re.sub(r'<\|endoftext\|>', '', content)
        content = re.sub(r'<\|end\|>', '', content)
        # Rimuovi tag <think>...</think>
        content = re.sub(r'(?s)<think>.*?</think>\s*', '', content)
        # Rimuovi backtick markdown
        content = re.sub(r'^\s*```[a-zA-Z]*\s*', '', content)
        content = re.sub(r'\s*```\s*$', '', content)
        content = content.strip()
 
        log.info(f"  Cleaned (first 200): {repr(content[:200])}")
 
        # ── Estrai JSON con regex (robusto) ──
        # Cerca il primo oggetto JSON {...} nella risposta
        json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', content)
        if json_match:
            json_str = json_match.group(0)
        else:
            log.warning(f"  Nessun JSON trovato nella risposta")
            return None
 
        result = json.loads(json_str)
 
        if "livello_rischio" not in result:
            log.warning("  Risposta manca livello_rischio")
            return None
 
        # ── Validazione coerenza ──
        # Un modello 4B spesso si contraddice. Correggiamo programmaticamente.
        presenza = result.get("presenza_umana", "NO").upper()
        tipo = result.get("tipo_soggetto", "")
        rischio = result.get("livello_rischio", "VERDE").upper()
 
        if presenza == "NO" and tipo not in ("veicolo",):
            # Nessuna persona e nessun veicolo → non può essere ROSSO o GIALLO
            if rischio in ("ROSSO", "GIALLO"):
                log.warning(f"  COERENZA: presenza_umana=NO ma rischio={rischio} → forzato VERDE")
                result["livello_rischio"] = "VERDE"
                result["azione_raccomandata"] = "ignora"
 
        if presenza == "SI" and tipo in ("animale",):
            # Se dice persona presente ma tipo=animale, c'è confusione
            log.warning(f"  COERENZA: presenza_umana=SI ma tipo=animale → forzato sconosciuto")
            result["tipo_soggetto"] = "sconosciuto"
 
        log.info(f"  Parsed OK: rischio={result.get('livello_rischio')} "
                 f"(presenza={presenza}, tipo={result.get('tipo_soggetto')})")
        return result
 
    except json.JSONDecodeError as e:
        log.error(f"  JSON DECODE ERROR: {e}")
        log.error(f"  Content was: {repr(content[:300])}")
        return None
    except Exception as e:
        log.error(f"  ERRORE: {type(e).__name__}: {e}")
        return None
 
 
def compare_risks(result_1: dict | None, result_2: dict | None) -> dict:
    """Confronta due analisi, il rischio più alto vince."""
 
    def get_risk®:
        if r and isinstance(r, dict):
            return r.get("livello_rischio", "INVALID").upper()
        return "INVALID"
 
    risk_1 = get_risk(result_1)
    risk_2 = get_risk(result_2)
 
    valid_risks = [r for r in [risk_1, risk_2]
                   if r in ("ROSSO", "GIALLO", "VERDE")]
 
    if "ROSSO" in valid_risks:
        final_risk = "ROSSO"
    elif "GIALLO" in valid_risks:
        final_risk = "GIALLO"
    elif "VERDE" in valid_risks:
        final_risk = "VERDE"
    else:
        final_risk = "GIALLO"
 
    if final_risk == risk_1 and result_1:
        analysis = result_1
    elif final_risk == risk_2 and result_2:
        analysis = result_2
    elif result_1:
        analysis = result_1
    elif result_2:
        analysis = result_2
    else:
        analysis = DEFAULT_RESULT.copy()
 
    risk_emoji = {"ROSSO": "", "GIALLO": "", "VERDE": ""}
 
    return {
        "presenza_umana": analysis.get("presenza_umana", "NO"),
        "tipo_soggetto": analysis.get("tipo_soggetto", "sconosciuto"),
        "posizione": analysis.get("posizione", "sconosciuta"),
        "distanza": analysis.get("distanza", "medio"),
        "livello_rischio": final_risk,
        "azione_raccomandata": analysis.get("azione_raccomandata", "controlla"),
        "descrizione_breve": analysis.get("descrizione_breve", "Rilevamento"),
        "risk_1": risk_1,
        "risk_2": risk_2,
        "final_risk": final_risk,
        "risk_color": risk_emoji.get(final_risk, ""),
        "debug_info": (
            f"T1:{risk_1}|T2:{risk_2}→{final_risk}|"
            f"{analysis.get('posizione', 'N/A')}"
            f"[{analysis.get('distanza', '?')}]"
        ),
        "success": True
    }
 
 
def write_error(output_path: str, msg: str):
    """Scrive un risultato di errore e esce."""
    result = {
        **DEFAULT_RESULT,
        "success": False,
        "debug_info": msg,
        "risk_1": "INVALID", "risk_2": "INVALID",
        "final_risk": "GIALLO", "risk_color": ""
    }
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    Path(output_path).write_text(json.dumps(result, ensure_ascii=False))
    log.error(msg)
 
 
def main():
    # ── Log TUTTO per debug ──
    log.info(f"{'='*60}")
    log.info(f"Script avviato")
    log.info(f"sys.argv ({len(sys.argv)} elementi):")
    for i, arg in enumerate(sys.argv):
        log.info(f"  [{i}] = '{arg}'" if len(arg) < 100 else f"  [{i}] = '{arg[:80]}...' ({len(arg)} chars)")
    log.info(f"CWD: {Path.cwd()}")
 
    # ── Parsing argomenti ──
    # Uso:  script.py <snapshot_path> <device_id> <alarm_state> <periodo> <output_path>
    # NESSUN argomento contiene spazi → nessun problema di quoting shell
 
    if len(sys.argv) < 6:
        msg = f"Args insufficienti: {len(sys.argv)-1}/5 — argv={sys.argv}"
        log.error(msg)
        # Scrivi errore anche nel file di output se possibile
        out = sys.argv[-1] if len(sys.argv) > 1 else "/config/www/tmp/llm_security_result.json"
        write_error(out, msg)
        sys.exit(1)
 
    snapshot_path = sys.argv[1]
    device_id     = sys.argv[2]
    alarm_state   = sys.argv[3]
    periodo       = sys.argv[4]
    output_path   = sys.argv[5]
 
    log.info(f"Parsed args:")
    log.info(f"  snapshot_path = '{snapshot_path}'")
    log.info(f"  device_id     = '{device_id}'")
    log.info(f"  alarm_state   = '{alarm_state}'")
    log.info(f"  periodo       = '{periodo}'")
    log.info(f"  output_path   = '{output_path}'")
 
    # ── Risolvi nomi e contesto dalla mappa interna ──
    camera_friendly = FRIENDLY_NAMES.get(device_id, device_id)
    camera_context  = CAMERA_CONTEXT_MAP.get(device_id, "Standard")
 
    log.info(f"═══ INIZIO ANALISI ═══")
    log.info(f"Camera: {camera_friendly} ({device_id})")
    log.info(f"Periodo: {periodo} | Allarme: {alarm_state}")
    log.info(f"Snapshot: {snapshot_path}")
    log.info(f"LM Studio: {LM_STUDIO_URL} | Model: {MODEL}")
 
    # ── Verifica immagine ──
    if not Path(snapshot_path).exists():
        write_error(output_path, f"Immagine non trovata: {snapshot_path}")
        sys.exit(1)
 
    # ── Codifica immagine ──
    log.info("Codifica base64...")
    try:
        image_b64 = encode_image_b64(snapshot_path)
    except Exception as e:
        write_error(output_path, f"Errore codifica: {e}")
        sys.exit(1)
    log.info(f"Immagine: {len(image_b64)} chars base64 "
             f"({Path(snapshot_path).stat().st_size} bytes)")
 
    # ── Test connettività rapido ──
    log.info(f"Test connettività {LM_STUDIO_URL}...")
    try:
        test_url = f"{LM_STUDIO_URL}/v1/models"
        if HAS_REQUESTS:
            test_resp = requests.get(test_url, timeout=5)
            log.info(f"Connettività OK: HTTP {test_resp.status_code}")
        else:
            with urllib.request.urlopen(test_url, timeout=5) as resp:
                log.info(f"Connettività OK: HTTP {resp.status}")
    except Exception as e:
        write_error(output_path, f"LM Studio non raggiungibile: {type(e).__name__}: {e}")
        sys.exit(1)
 
    # ── Costruisci prompt ──
    prompt = build_prompt(camera_friendly, alarm_state, periodo, camera_context)
 
    # ── Prima analisi ──
    log.info(f"── Chiamata 1/2 (T={TEMPERATURES[0]}) ──")
    t0 = time.time()
    result_1 = call_lm_studio(image_b64, prompt, TEMPERATURES[0])
    dt1 = time.time() - t0
    log.info(f"Chiamata 1: {dt1:.1f}s → "
             f"{result_1.get('livello_rischio', 'FAIL') if result_1 else 'FAIL'}")
 
    # ── Seconda analisi ──
    log.info(f"── Chiamata 2/2 (T={TEMPERATURES[1]}) ──")
    t0 = time.time()
    result_2 = call_lm_studio(image_b64, prompt, TEMPERATURES[1])
    dt2 = time.time() - t0
    log.info(f"Chiamata 2: {dt2:.1f}s → "
             f"{result_2.get('livello_rischio', 'FAIL') if result_2 else 'FAIL'}")
 
    # ── Confronto ──
    consolidated = compare_risks(result_1, result_2)
    log.info(f"═══ RISULTATO: {consolidated['debug_info']} "
             f"(totale: {dt1+dt2:.1f}s) ═══")
 
    # ── Scrivi output ──
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    Path(output_path).write_text(json.dumps(consolidated, ensure_ascii=False))
 
 
if __name__ == "__main__":
    main()

 

Inutile spostare in variabili esterne il context delle telecamere, l'ip di lm studio e così via. E' roba che deve girare solo per me.

 

 

Questo è quanto aggiunto in configuration.yaml:

 

input_text:

  security_device_id:
    name: "Security Device ID"
    max: 255
    initial: ""
  security_snapshot_path:
    name: "Security Snapshot Path"
    max: 255
    initial: ""
 
shell_command:
  hikvision_isapi_snap: >
    curl --digest -u "admin:VVVCIC1a!" -X GET "http://10.0.40.16/IS...ing/channels/{{channel }}01/picture" -o "{{ filename }}" --connect-timeout 5
  clean_old_snapshots: >
    find /config/www/tmp/ -name "security_*.jpg" -mtime +7 -delete
  lm_analyze_security: >-
    python3 /config/scripts/lm_studio_analyze.py
    "{{ states('input_text.security_snapshot_path') }}"
    "{{ states('input_text.security_device_id') }}"
    "{{ states('alarm_control_panel.risco_bosco_paolo_partition_0') }}"
    "{{ 'NOTTE' if is_state('sun.sun', 'below_horizon') else 'GIORNO' }}"
    "/config/www/tmp/llm_security_result.json"
 
 
command_line:
  - sensor:
      name: "LM Security Result"
      unique_id: lm_security_result
      command: >-
        cat /config/www/tmp/llm_security_result.json 2>/dev/null ||
        echo '{"livello_rischio":"GIALLO","debug_info":"Nessun dato","success":false}'
      value_template: "{{ value_json.livello_rischio | default('GIALLO') }}"
      json_attributes:
        - presenza_umana
        - tipo_soggetto
        - posizione
        - distanza
        - livello_rischio
        - azione_raccomandata
        - descrizione_breve
        - risk_1
        - risk_2
        - final_risk
        - risk_color
        - debug_info
        - success
      scan_interval: 31536000

 

 

Questa l'automazione:

 

alias: Sicurezza Perimetrale - Doppio Check AI Locale (v2)

description: >-
  Analisi doppio check Qwen3.5-4b-mlx via LM Studio locale. Usa input_text come
  ponte per passare device_id e snapshot_path dall'automazione allo
  shell_command.
triggers:
  - entity_id:
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_5_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_1_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_6_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_9_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_4_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_7_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_2_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_8_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_3_fielddetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_5_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_9_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_6_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_4_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_7_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_2_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_8_linedetection
      - binary_sensor.ds_7616ni_m21620230321ccrrl43839803wcvu_3_linedetection
    to: "on"
    id: security_event
    trigger: state
conditions:
  - condition: template
    value_template: "{{ true }}"
  - condition: or
    conditions:
      - condition: not
        conditions:
          - condition: state
            entity_id: device_tracker.papoy
            state: home
          - condition: state
            entity_id: device_tracker.pixel_10_pro
            state: home
      - condition: state
        entity_id: sun.sun
        state: below_horizon
actions:
  - data:
      channel: "{{ nvr_channel }}"
      filename: "{{ snapshot_path }}"
    action: shell_command.hikvision_isapi_snap
  - action: input_text.set_value
    target:
      entity_id: input_text.security_device_id
    data:
      value: "{{ device_id }}"
  - action: input_text.set_value
    target:
      entity_id: input_text.security_snapshot_path
    data:
      value: "{{ snapshot_path }}"
  - delay:
      milliseconds: 500
  - action: shell_command.lm_analyze_security
  - action: homeassistant.update_entity
    data:
      entity_id: sensor.lm_security_result
  - delay:
      milliseconds: 300
  - variables:
      final_risk: >-
        {{ state_attr('sensor.lm_security_result', 'final_risk') |
        default('GIALLO') }}
      analysis_tipo: >-
        {{ state_attr('sensor.lm_security_result', 'tipo_soggetto') |
        default('sconosciuto') }}
      analysis_distanza: >-
        {{ state_attr('sensor.lm_security_result', 'distanza') | default('?') }}
      analysis_descr: >-
        {{ state_attr('sensor.lm_security_result', 'descrizione_breve') |
        default('Rilevamento') }}
      analysis_posizione: >-
        {{ state_attr('sensor.lm_security_result', 'posizione') | default('N/A')
        }}
      analysis_azione: >-
        {{ state_attr('sensor.lm_security_result', 'azione_raccomandata') |
        default('controlla') }}
      risk_color: >-
        {{ state_attr('sensor.lm_security_result', 'risk_color') | default('')
        }}
      debug_info: >-
        {{ state_attr('sensor.lm_security_result', 'debug_info') |
        default('N/A') }}
  - target:
      entity_id: input_datetime.last_security_{{ device_id }}
    data:
      datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
    action: input_datetime.set_datetime
  - action: notify.mobile_app_pixel_10_pro
    data:
      title: "{{ risk_color }}{{ final_risk }}|{{ camera_friendly }}"
      message: "{{ analysis_tipo }} ({{ analysis_distanza }}): {{ analysis_descr }}"
      data:
        priority: high
        importance: high
        channel: Sicurezza_AI
        visibility: public
        notification_icon: mdi:shield-alert
        color: >-
          {{ 'red' if final_risk == 'ROSSO' else ('yellow' if final_risk ==
          'GIALLO' else 'green') }}
        image: /local/tmp/{{ snapshot_path.split('/') | last }}
        clickAction: /local/tmp/{{ snapshot_path.split('/') | last }}
        sticky: true
        ongoing: "{{ true if final_risk == 'ROSSO' else false }}"
        tag: sec_img_{{ device_id }}_{{ now().strftime('%H%M%S') }}
  - action: notify.mobile_app_pixel_10_pro
    data:
      title: Debug | {{ camera_friendly }}
      message: "{{ debug_info }} | Azione: {{ analysis_azione }}"
      data:
        priority: high
        importance: high
        channel: Sicurezza_AI_Debug
        visibility: public
        notification_icon: mdi:information-outline
        color: gray
        tag: sec_dbg_{{ device_id }}_{{ now().strftime('%H%M%S') }}
  - data:
      level: info
      message: "[SEC-AI-LOCAL] {{ camera_friendly }} | {{ debug_info }}"
    action: system_log.write
variables:
  device_id: |-
    {{ trigger.entity_id.split('.')[1] 
       | regex_replace('_fielddetection$', '') 
       | regex_replace('_linedetection$', '') }}
  friendly_names:
    ds_7616ni_m21620230321ccrrl43839803wcvu_1: Cortile Nord
    ds_7616ni_m21620230321ccrrl43839803wcvu_2: Giardino Ovest
    ds_7616ni_m21620230321ccrrl43839803wcvu_3: Giardino Sud
    ds_7616ni_m21620230321ccrrl43839803wcvu_4: Cortile Ovest
    ds_7616ni_m21620230321ccrrl43839803wcvu_5: Cancelletto
    ds_7616ni_m21620230321ccrrl43839803wcvu_6: Cortile Sud
    ds_7616ni_m21620230321ccrrl43839803wcvu_7: Giardino Est
    ds_7616ni_m21620230321ccrrl43839803wcvu_8: Giardino Nord
    ds_7616ni_m21620230321ccrrl43839803wcvu_9: Cortile Est
  channel_mapping:
    ds_7616ni_m21620230321ccrrl43839803wcvu_1: "1"
    ds_7616ni_m21620230321ccrrl43839803wcvu_2: "2"
    ds_7616ni_m21620230321ccrrl43839803wcvu_3: "3"
    ds_7616ni_m21620230321ccrrl43839803wcvu_4: "4"
    ds_7616ni_m21620230321ccrrl43839803wcvu_5: "5"
    ds_7616ni_m21620230321ccrrl43839803wcvu_6: "6"
    ds_7616ni_m21620230321ccrrl43839803wcvu_7: "7"
    ds_7616ni_m21620230321ccrrl43839803wcvu_8: "8"
    ds_7616ni_m21620230321ccrrl43839803wcvu_9: "9"
  nvr_channel: "{{ channel_mapping.get(device_id, '1') }}"
  camera_friendly: "{{ friendly_names.get(device_id, device_id) }}"
  snapshot_path: >-
    /config/www/tmp/security_{{ device_id }}_{{ now().strftime('%Y%m%d_%H%M%S')
    }}.jpg
mode: queued
max: 5

 

Per chi proprio ne sentisse il bisogno: 1,5 ore di lavoro tra le 4 e le 5.30 di mattina di domenica con Opus.


  • toyo likes this

If you're smart enough you'll survive to get bigger. If you're not then you're just part of the food chain.


#3102 euthanasia

euthanasia

    Matto

  • Donatori di sperma
  • PipPipPipPipPipPipPipPip
  • 50612 posts

Posted Yesterday, 13:48

Letto tutto :sisi:
  • harbinger likes this

#3103 Pistus

Pistus

    Bello di Zio

  • Membri
  • PipPipPipPipPipPipPip
  • 25916 posts

Posted Yesterday, 14:08

Letto tutto :sisi:

fake altrimenti avresti commentato la riga 412 dove ha scritto volontariamente sborra di negro


Sborrare e.t divertire  :megusta:


 


#3104 eglio

eglio

    Schiavo

  • Membri
  • PipPipPipPipPipPipPip
  • 44281 posts

Posted Today, 08:45

HomePad 7": ancora 6 mesi di attesa
 
Home-Pad.png

 

Il display, che assomiglia a un iPad quadrato, può essere fissato a una base con altoparlante a forma di mezza cupola oppure a un supporto da parete ed è progettato per essere un hub centrale di intelligenza artificiale per la casa. L’interfaccia utente include un elenco di icone circolari delle app disposte in modo simile alla schermata Home di un crapple Watch.

La caratteristica principale è un sistema basato sul riconoscimento facciale in grado di riconoscere le persone quando si avvicinano al dispositivo. Grazie a queste informazioni, il prodotto può quindi mostrare dati personalizzati, come gli appuntamenti del calendario dell’utente, promemoria, note, preferenze musicali e notizie.

crapple sta attualmente pianificando di lanciare il display per la smart home con una variante di tvOS 27, una versione del sistema operativo per il suo set-top box TV prevista per il rilascio a settembre. Il nuovo sistema operativo domestico di crapple è basato sulla tecnologia di base presente su crapple TV.

Fino ai recenti ritardi legati a Siri, crapple aveva pianificato che i primi d ispositivi domestici funzionassero con una variante di tvOS 26, l’attuale software di crapple TV. John Ternus, vicepresidente senior dell’ingegneria hardware, ha avuto un ruolo centrale negli sforzi per la smart home e considera questo settore fondamentale per la crescita futura dell’azienda.

crapple puntava ad avere tutte le nuove funzionalità di Siri pronte per l’aggiornamento 26.4 del software quest’anno, ma ora le sta testando per le versioni 26.5 e 27, secondo quanto riportato da Bloomberg News.

Il display per la smart home ha uno schermo da 7 pollici, una singola porta USB-C per l’alimentazione e la classica scocca in alluminio argentato tipica di crapple. Sarà il primo di diversi dispositivi domestici crapple. È prevista per il prossimo anno anche una versione con schermo da 9 pollici collegato a un braccio robotico. Inoltre, è in sviluppo anche un piccolo sensore di sicurezza domestica.



Voi prenderete l'HomePad 7" oppure aspettate l'HomePad Pro 9" su braccio robotico rotante ed emotivamente-ballonzolante-come-una-Pixar-lamp-animata?

Secondo voi per alimentare l'HomePad 7" basterà una usb-c da 20W (come per gli HomePod Mini) o servirà una 30W? Vi ricordo che il display dovrebbe avere anche una batteria interna per facilitare il montaggio a muro 🤔

Voi quanti ne comprerete dei piccolissimi sensori di presenza/movimento AppIe Home Security Sensor basati su rete Thread? Io penso una dozzina, sicuramente connettendosi all'HomePad faranno cose parecchio smart a livello sia di automazioni che di sicurezza.


Un giorno qualcuno mi spegherà perchè dire la verità che i russi sono più forti, stanno vincendo e vinceranno la guerra, vuol dire anche essere filorussi

ciò che afferma Putin, drammaticamente, corrisponde a una verità più imparentata con la realtà e la logica.


#3105 Guren

Guren

    Da schiavo a schiavista

  • Grandi donatori di sperma
  • PipPipPipPipPipPipPip
  • 30257 posts

Posted Today, 08:53

tipo avvisarti che mammà sta percorrendo il corridoio verso la tua cameretta così hai tempo di togliere il dildo da 8cm di diametro dal culo mentre ti fai le pippe?
  • Manson likes this
In tutta la sua vita,

quante volte può riuscire un uomo

a fermare le lacrime di una donna?

#3106 harbinger

harbinger

    Rompicoglioni DOCG

  • Grandi donatori di sperma
  • PipPipPipPipPipPipPip
  • 49894 posts

Posted Today, 08:58

HomePad 7": ancora 6 mesi di attesa
 
Home-Pad.png

 



Voi prenderete l'HomePad 7" oppure aspettate l'HomePad Pro 9" su braccio robotico rotante ed emotivamente-ballonzolante-come-una-Pixar-lamp-animata?

Secondo voi per alimentare l'HomePad 7" basterà una usb-c da 20W (come per gli HomePod Mini) o servirà una 30W? Vi ricordo che il display dovrebbe avere anche una batteria interna per facilitare il montaggio a muro

Voi quanti ne comprerete dei piccolissimi sensori di presenza/movimento AppIe Home Security Sensor basati su rete Thread? Io penso una dozzina, sicuramente connettendosi all'HomePad faranno cose parecchio smart a livello sia di automazioni che di sicurezza.

 

Quindi, crapple sta entrando nell'anno 2016 della domotica, a occhio e croce. Peraltro, nel 2024 dell'AI, considerate le performance di Gemini 3.1 Pro nell'eseguire una catena di compiti, nella "difficoltà" (anche un po' di più) di usare mcp, nella tendenza ad allucinare moltissimo col codice e nella ferma programmazione a fare meno ricerche possibili per risparmiare?


If you're smart enough you'll survive to get bigger. If you're not then you're just part of the food chain.


#3107 Guren

Guren

    Da schiavo a schiavista

  • Grandi donatori di sperma
  • PipPipPipPipPipPipPip
  • 30257 posts

Posted Today, 09:05

ehi ma sembra un iPad quadrato, vuoi mettere?
In tutta la sua vita,

quante volte può riuscire un uomo

a fermare le lacrime di una donna?

#3108 balint

balint

    Schiavo

  • Membri
  • PipPipPipPipPipPipPipPip
  • 55935 posts

Posted Today, 09:16

mi ricorda Uebbi

 

https://www.thenet.i...I_HWTEl980.html


  • eglio likes this

#3109 eglio

eglio

    Schiavo

  • Membri
  • PipPipPipPipPipPipPip
  • 44281 posts

Posted Today, 09:45

Quindi, crapple sta entrando nell'anno 2016 della domotica, a occhio e croce. Peraltro, nel 2024 dell'AI, considerate le performance di Gemini 3.1 Pro nell'eseguire una catena di compiti, nella "difficoltà" (anche un po' di più) di usare mcp, nella tendenza ad allucinare moltissimo col codice e nella ferma programmazione a fare meno ricerche possibili per risparmiare?

 

Vediamo come se la cava Gemini-in-salsa-crapple prima di fasciarci la testa :P

 

Per quel che riguarda il "codice", si può dire che crapple non incoraggi l'utilizzo di Gemini.

In Xcode 26.3 infatti Claude e Codex sono integrati nativamente, mentre Gemini si può integrare tramite MCP bridge.

 

crapple ha scelto Gemini per le robe che non attengono al "codice".

 

Sull'HomePad, ovvero la rivisitazione crapple del formato "Echo Show" (ma fatto da chi sa fare bene gli OS, le piattaforme, le app, le interfacce utente veloci, i riconoscimenti facciali, ecc.), dubito che qualcuno scriverà "codice" :D


Un giorno qualcuno mi spegherà perchè dire la verità che i russi sono più forti, stanno vincendo e vinceranno la guerra, vuol dire anche essere filorussi

ciò che afferma Putin, drammaticamente, corrisponde a una verità più imparentata con la realtà e la logica.


#3110 toyo

toyo

    sono triste

  • Donatori di sperma
  • PipPipPipPipPipPipPip
  • 46700 posts

Posted Today, 10:10

capirai, xcode chi lo usa ormai

FIRMA FOTTUTAMENTE EDITATA. IL FOTTUTO STAFF.
 

Mai più giorni felici


#3111 eglio

eglio

    Schiavo

  • Membri
  • PipPipPipPipPipPipPip
  • 44281 posts

Posted Today, 10:26

A proposito, visto che per sviluppare app per piattaforme crapple è obbligatorio avere un Mac e Xcode, ci pensate quanto si è abbassata la barriera di ingresso col MacBook Neo? :eheh:

Magari il prossimo sviluppatore di una app da un milione di dollari è un ragazzino di 14 anni che poteva permettersi di iniziare solo su un MacBook Neo e non su MacBook più costosi 🚀

Edited by eglio, Today, 10:27.

Un giorno qualcuno mi spegherà perchè dire la verità che i russi sono più forti, stanno vincendo e vinceranno la guerra, vuol dire anche essere filorussi

ciò che afferma Putin, drammaticamente, corrisponde a una verità più imparentata con la realtà e la logica.


#3112 euthanasia

euthanasia

    Matto

  • Donatori di sperma
  • PipPipPipPipPipPipPipPip
  • 50612 posts

Posted Today, 14:54

penso una dozzina


Suuure

per quanto ne sappiamo noi, potresti anche non avere alcun genere di prodotto crapple in casa

#3113 harbinger

harbinger

    Rompicoglioni DOCG

  • Grandi donatori di sperma
  • PipPipPipPipPipPipPip
  • 49894 posts

Posted Today, 15:06

Vediamo come se la cava Gemini-in-salsa-crapple prima di fasciarci la testa :P

 

Per quel che riguarda il "codice", si può dire che crapple non incoraggi l'utilizzo di Gemini.

In Xcode 26.3 infatti Claude e Codex sono integrati nativamente, mentre Gemini si può integrare tramite MCP bridge.

 

crapple ha scelto Gemini per le robe che non attengono al "codice".

 

Sull'HomePad, ovvero la rivisitazione crapple del formato "Echo Show" (ma fatto da chi sa fare bene gli OS, le piattaforme, le app, le interfacce utente veloci, i riconoscimenti facciali, ecc.), dubito che qualcuno scriverà "codice" :D

 

Quindi, Gemini per l'intrattenimento. Faccine personalizzate di ogni colore per vendere di più? riassunti delle email per chi non riesce più a leggere? consulti psicologici con l'IA perché si è tutti stressati? può funzionare finché gli USA rimangono pieni di benestanti e si continua una strettissima compliance con la RPC.


If you're smart enough you'll survive to get bigger. If you're not then you're just part of the food chain.





3 user(s) are reading this topic

0 members, 3 guests, 0 anonymous users