Plugin visione: webcam, schermo e finestra con OCR e immagine per i motori
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
64f3474d92
commit
8687c895a1
8 files changed
+193
-2
No files matched your search
@@ -72,6 +72,7 @@ Altri plugin inclusi, tutti locali:
|
|||||||
| `buongiorno` | riepilogo: impegni, promemoria, email, meteo | "buongiorno", "come si presenta la giornata" |
|
| `buongiorno` | riepilogo: impegni, promemoria, email, meteo | "buongiorno", "come si presenta la giornata" |
|
||||||
| `whatsapp_archivio` | chat WhatsApp esportate: importazione, ricerca full-text, lettura per periodo, statistiche | "cosa mi ha detto Marco sulla riunione?", "riassumi la chat con Anna dell'ultima settimana", "quando abbiamo parlato del dentista?" |
|
| `whatsapp_archivio` | chat WhatsApp esportate: importazione, ricerca full-text, lettura per periodo, statistiche | "cosa mi ha detto Marco sulla riunione?", "riassumi la chat con Anna dell'ultima settimana", "quando abbiamo parlato del dentista?" |
|
||||||
| `whatsapp_live` | WhatsApp in tempo reale come dispositivo collegato (Baileys): novità, invio con conferma, annunci vocali | "ci sono novità su WhatsApp?", "chi mi ha scritto?", "scrivi a Marco su WhatsApp che arrivo" |
|
| `whatsapp_live` | WhatsApp in tempo reale come dispositivo collegato (Baileys): novità, invio con conferma, annunci vocali | "ci sono novità su WhatsApp?", "chi mi ha scritto?", "scrivi a Marco su WhatsApp che arrivo" |
|
||||||
|
| `visione` | webcam, schermo intero o finestra in primo piano: immagine per i motori con visione (Claude API, Claude Code) più testo riconosciuto per tutti | "cosa vedi?", "cosa c'è sullo schermo?", "leggi questo errore", "riassumi questa finestra" |
|
||||||
| `telegram` | Telegram con il tuo account (Telethon): non letti, lettura, ricerca, invio con conferma | "ho messaggi su Telegram?", "leggimi la chat con Anna", "scrivi a Luca su Telegram che sono in ritardo" |
|
| `telegram` | Telegram con il tuo account (Telethon): non letti, lettura, ricerca, invio con conferma | "ho messaggi su Telegram?", "leggimi la chat con Anna", "scrivi a Luca su Telegram che sono in ritardo" |
|
||||||
|
|
||||||
**WhatsApp in tempo reale** usa un client non ufficiale (Baileys, in `whatsapp_bridge/`, Node.js) collegato come "dispositivo collegato". È contro i termini di WhatsApp e Meta può bloccare il numero: l'app non invia mai senza conferma e non fa azioni di massa, ma il rischio resta a carico di chi lo attiva. Per collegarlo: "Motore e Voce" › WhatsApp › "Collega (QR)", poi sul telefono WhatsApp › Impostazioni › Dispositivi collegati › Collega un dispositivo. Da quel momento i messaggi in arrivo (e quelli che invii da altri dispositivi) finiscono nell'archivio, in tempo reale; con "Annuncia a voce" l'assistente li legge appena arrivano. Il ponte parte da solo all'avvio dell'app una volta collegato. Richiede Node.js (`npm install` in `whatsapp_bridge/` è già fatto).
|
**WhatsApp in tempo reale** usa un client non ufficiale (Baileys, in `whatsapp_bridge/`, Node.js) collegato come "dispositivo collegato". È contro i termini di WhatsApp e Meta può bloccare il numero: l'app non invia mai senza conferma e non fa azioni di massa, ma il rischio resta a carico di chi lo attiva. Per collegarlo: "Motore e Voce" › WhatsApp › "Collega (QR)", poi sul telefono WhatsApp › Impostazioni › Dispositivi collegati › Collega un dispositivo. Da quel momento i messaggi in arrivo (e quelli che invii da altri dispositivi) finiscono nell'archivio, in tempo reale; con "Annuncia a voce" l'assistente li legge appena arrivano. Il ponte parte da solo all'avvio dell'app una volta collegato. Richiede Node.js (`npm install` in `whatsapp_bridge/` è già fatto).
|
||||||
@@ -88,7 +89,7 @@ Per scriverne uno nuovo copia la struttura di `plugins/calendario.py`: una lista
|
|||||||
|
|
||||||
- macOS su Apple Silicon, Python 3.12, [uv](https://docs.astral.sh/uv/).
|
- macOS su Apple Silicon, Python 3.12, [uv](https://docs.astral.sh/uv/).
|
||||||
- `brew install espeak-ng portaudio` (fonetica italiana per Kokoro e audio).
|
- `brew install espeak-ng portaudio` (fonetica italiana per Kokoro e audio).
|
||||||
- Permessi macOS richiesti al primo uso: Microfono, Calendario, Promemoria, Contatti, Automazione; per leggere SMS e iMessage serve «Accesso completo al disco» all'app (Impostazioni di Sistema › Privacy e sicurezza).
|
- Permessi macOS richiesti al primo uso: Microfono, Fotocamera, Registrazione schermo, Calendario, Promemoria, Contatti, Automazione; per leggere SMS e iMessage serve «Accesso completo al disco» all'app (Impostazioni di Sistema › Privacy e sicurezza).
|
||||||
- Per il motore Claude Code: Claude Code installato e collegato.
|
- Per il motore Claude Code: Claude Code installato e collegato.
|
||||||
|
|
||||||
## Avvio
|
## Avvio
|
||||||
|
|||||||
@@ -23,6 +23,22 @@ class Aborted(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_content(res: str):
|
||||||
|
"""Se il risultato cita un'immagine (riga «IMMAGINE: percorso»), la allega come blocco image."""
|
||||||
|
import base64
|
||||||
|
import re as _re
|
||||||
|
from pathlib import Path as _P
|
||||||
|
m = _re.search(r"^IMMAGINE: (.+)$", res, flags=_re.M)
|
||||||
|
if not m or not _P(m.group(1).strip()).exists():
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
from avatar.attachments import image_payload
|
||||||
|
media, data = image_payload(_P(m.group(1).strip()))
|
||||||
|
return [{"type": "text", "text": res}, {"type": "image", "source": {"type": "base64", "media_type": media, "data": base64.b64encode(data).decode()}}]
|
||||||
|
except Exception:
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
def friendly_error(err: Exception) -> str:
|
def friendly_error(err: Exception) -> str:
|
||||||
if isinstance(err, anthropic.AuthenticationError):
|
if isinstance(err, anthropic.AuthenticationError):
|
||||||
return "La chiave API Anthropic non è valida. Controllala nelle impostazioni."
|
return "La chiave API Anthropic non è valida. Controllala nelle impostazioni."
|
||||||
@@ -147,7 +163,7 @@ class AnthropicEngine:
|
|||||||
res, ev = run_tool(b.name, parse_args(b.input))
|
res, ev = run_tool(b.name, parse_args(b.input))
|
||||||
if ev:
|
if ev:
|
||||||
emit(ev)
|
emit(ev)
|
||||||
results.append({"type": "tool_result", "tool_use_id": b.id, "content": res})
|
results.append({"type": "tool_result", "tool_use_id": b.id, "content": _tool_content(res)})
|
||||||
self.history.messages.append({"role": "user", "content": results})
|
self.history.messages.append({"role": "user", "content": results})
|
||||||
emit({"type": "status", "status": "thinking"})
|
emit({"type": "status", "status": "thinking"})
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,18 @@ class ClaudeCodeEngine:
|
|||||||
f"Adesso è {today_label()}.",
|
f"Adesso è {today_label()}.",
|
||||||
"Strumenti di memoria (obbligatori): per ricordare un fatto sull'utente chiama `mcp__avatar__salva_memoria`; per cancellarne uno `mcp__avatar__dimentica_memoria`; per cercare `mcp__avatar__cerca_memoria`. Non dire mai di aver salvato qualcosa senza averlo chiamato davvero. Gli altri strumenti `mcp__avatar__*` (per esempio il calendario) agiscono sul Mac dell'utente: usali quando servono, e per le azioni irreversibili chiedi conferma a voce prima di richiamarli con confermato=true.",
|
"Strumenti di memoria (obbligatori): per ricordare un fatto sull'utente chiama `mcp__avatar__salva_memoria`; per cancellarne uno `mcp__avatar__dimentica_memoria`; per cercare `mcp__avatar__cerca_memoria`. Non dire mai di aver salvato qualcosa senza averlo chiamato davvero. Gli altri strumenti `mcp__avatar__*` (per esempio il calendario) agiscono sul Mac dell'utente: usali quando servono, e per le azioni irreversibili chiedi conferma a voce prima di richiamarli con confermato=true.",
|
||||||
access,
|
access,
|
||||||
|
"Quando uno strumento risponde con una riga «IMMAGINE: percorso», leggi quel file con Read per vedere l'immagine prima di rispondere.",
|
||||||
"Stai parlando dentro un'app vocale: rispondi in modo conversazionale, senza intestazioni Markdown né tabelle, e non citare nomi di file o strumenti interni a meno che non serva.",
|
"Stai parlando dentro un'app vocale: rispondi in modo conversazionale, senza intestazioni Markdown né tabelle, e non citare nomi di file o strumenti interni a meno che non serva.",
|
||||||
])
|
])
|
||||||
|
|
||||||
def _args(self, session_id: str, resume: bool, attach_path: str = "") -> list[str]:
|
def _args(self, session_id: str, resume: bool, attach_path: str = "") -> list[str]:
|
||||||
flags = dict(ACCESS[self.access])
|
flags = dict(ACCESS[self.access])
|
||||||
|
# Le catture di webcam/schermo del plugin visione sono sempre leggibili.
|
||||||
|
from avatar.settings import DATA_DIR
|
||||||
|
captures = str(DATA_DIR / "captures")
|
||||||
|
if "Read" not in flags["tools"] and flags["tools"] != "default":
|
||||||
|
flags["tools"] = flags["tools"] + ",Read"
|
||||||
|
flags["allowed"] = [*flags["allowed"], f"Read(//{captures.lstrip('/')}/**)"]
|
||||||
if attach_path:
|
if attach_path:
|
||||||
# Permesso di lettura limitato al file allegato (immagini incluse: Claude Code le vede).
|
# Permesso di lettura limitato al file allegato (immagini incluse: Claude Code le vede).
|
||||||
if "Read" not in flags["tools"] and flags["tools"] != "default":
|
if "Read" not in flags["tools"] and flags["tools"] != "default":
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""Cattura di webcam, schermo e finestra in primo piano, con OCR e anteprima per i motori."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from avatar.settings import DATA_DIR
|
||||||
|
|
||||||
|
CAPTURES = DATA_DIR / "captures"
|
||||||
|
KEEP = 30
|
||||||
|
|
||||||
|
|
||||||
|
def _prune() -> None:
|
||||||
|
files = sorted(CAPTURES.glob("*"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
for p in files[KEEP:]:
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _shrink(src: Path, max_side: int = 1600, quality: int = 85) -> Path:
|
||||||
|
"""Versione JPEG ridotta, adatta ai modelli con visione."""
|
||||||
|
from PIL import Image
|
||||||
|
im = Image.open(src).convert("RGB")
|
||||||
|
im.thumbnail((max_side, max_side))
|
||||||
|
out = src.with_suffix(".jpg") if src.suffix.lower() != ".jpg" else src
|
||||||
|
im.save(out, format="JPEG", quality=quality)
|
||||||
|
if out != src:
|
||||||
|
src.unlink(missing_ok=True)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def capture_webcam(warmup_frames: int = 8) -> Path:
|
||||||
|
import cv2
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
cap = cv2.VideoCapture(0)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise RuntimeError("Webcam non disponibile: controlla il permesso Fotocamera in Impostazioni di Sistema > Privacy e sicurezza.")
|
||||||
|
try:
|
||||||
|
frame = None
|
||||||
|
for _ in range(warmup_frames): # qualche fotogramma per l'esposizione automatica
|
||||||
|
ok, frame = cap.read()
|
||||||
|
if not ok:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
if frame is None:
|
||||||
|
raise RuntimeError("La webcam non ha restituito immagini.")
|
||||||
|
out = CAPTURES / f"webcam-{time.strftime('%Y%m%d-%H%M%S')}.jpg"
|
||||||
|
cv2.imwrite(str(out), frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
|
||||||
|
finally:
|
||||||
|
cap.release()
|
||||||
|
_prune()
|
||||||
|
return _shrink(out)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_screen(all_displays: bool = False) -> Path:
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp = time.strftime("%Y%m%d-%H%M%S")
|
||||||
|
out = CAPTURES / f"schermo-{stamp}.png"
|
||||||
|
cmd = ["screencapture", "-x", str(out)] if not all_displays else ["screencapture", "-x", str(out), str(CAPTURES / f"schermo2-{stamp}.png")]
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
|
||||||
|
if res.returncode != 0 or not out.exists():
|
||||||
|
raise RuntimeError("Cattura schermo non riuscita: concedi «Registrazione schermo» all'app in Impostazioni di Sistema > Privacy e sicurezza.")
|
||||||
|
_prune()
|
||||||
|
return _shrink(out, max_side=2000)
|
||||||
|
|
||||||
|
|
||||||
|
def _front_window_id() -> tuple[int | None, str]:
|
||||||
|
import Quartz
|
||||||
|
from AppKit import NSWorkspace
|
||||||
|
app = NSWorkspace.sharedWorkspace().frontmostApplication()
|
||||||
|
name = app.localizedName() if app else ""
|
||||||
|
pid = app.processIdentifier() if app else None
|
||||||
|
for w in Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID):
|
||||||
|
if w.get("kCGWindowOwnerPID") == pid and w.get("kCGWindowLayer", 0) == 0 and (w.get("kCGWindowBounds") or {}).get("Height", 0) > 50:
|
||||||
|
return w["kCGWindowNumber"], name
|
||||||
|
return None, name
|
||||||
|
|
||||||
|
|
||||||
|
def capture_front_window() -> tuple[Path, str]:
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
wid, name = _front_window_id()
|
||||||
|
if wid is None:
|
||||||
|
return capture_screen(), name or "schermo"
|
||||||
|
out = CAPTURES / f"finestra-{time.strftime('%Y%m%d-%H%M%S')}.png"
|
||||||
|
res = subprocess.run(["screencapture", "-x", "-l", str(wid), str(out)], capture_output=True, text=True, timeout=20)
|
||||||
|
if res.returncode != 0 or not out.exists():
|
||||||
|
raise RuntimeError("Cattura finestra non riuscita: serve il permesso «Registrazione schermo».")
|
||||||
|
_prune()
|
||||||
|
return _shrink(out, max_side=2000), name
|
||||||
|
|
||||||
|
|
||||||
|
def ocr_text(path: Path) -> str:
|
||||||
|
from avatar.attachments import ocr
|
||||||
|
try:
|
||||||
|
return ocr(path).strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""Visione: guarda dalla webcam, lo schermo o la finestra in primo piano (immagine + testo riconosciuto)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from avatar import vision # noqa: E402
|
||||||
|
|
||||||
|
NOTE = ("Descrivi ciò che vedi in modo naturale. Se il motore non può vedere le immagini, basati sul testo riconosciuto.")
|
||||||
|
|
||||||
|
|
||||||
|
def _result(path: Path, label: str, ctx: dict) -> str:
|
||||||
|
text = vision.ocr_text(path)
|
||||||
|
try:
|
||||||
|
ui = ctx.get("player")
|
||||||
|
if ui is not None and hasattr(ui, "show_camera_frame"):
|
||||||
|
ui.show_camera_frame(path.read_bytes())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return (f"{label}\nIMMAGINE: {path}\n"
|
||||||
|
f"Testo riconosciuto nell'immagine:\n{text or '(nessun testo)'}\n{NOTE}")
|
||||||
|
|
||||||
|
|
||||||
|
def webcam(params: dict, ctx: dict) -> str:
|
||||||
|
path = vision.capture_webcam()
|
||||||
|
return _result(path, "Fotogramma dalla webcam (l'utente e il suo ambiente).", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def schermo(params: dict, ctx: dict) -> str:
|
||||||
|
path = vision.capture_screen(bool(params.get("tutti_gli_schermi")))
|
||||||
|
return _result(path, "Cattura dello schermo del Mac (il computer dell'utente, non una foto dell'utente).", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def finestra(params: dict, ctx: dict) -> str:
|
||||||
|
path, name = vision.capture_front_window()
|
||||||
|
return _result(path, f"Cattura della finestra in primo piano: {name or 'sconosciuta'}.", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
{"name": "guarda_webcam", "description": "Scatta un fotogramma dalla webcam per vedere l'utente e l'ambiente attorno a lui. Usalo per 'cosa vedi?', 'guarda questo', 'come sto?'.",
|
||||||
|
"parameters": {"type": "object", "properties": {}}, "run": webcam},
|
||||||
|
{"name": "guarda_schermo", "description": "Cattura lo schermo del Mac per vedere cosa c'è aperto: 'cosa c'è sullo schermo?', 'leggi questo errore', 'riassumi questa pagina'.",
|
||||||
|
"parameters": {"type": "object", "properties": {"tutti_gli_schermi": {"type": "boolean"}}}, "run": schermo},
|
||||||
|
{"name": "guarda_finestra", "description": "Cattura solo la finestra dell'app in primo piano (più leggibile dello schermo intero).",
|
||||||
|
"parameters": {"type": "object", "properties": {}}, "run": finestra},
|
||||||
|
]
|
||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"mlx-whisper>=0.4.3",
|
"mlx-whisper>=0.4.3",
|
||||||
"numpy>=2.5.3",
|
"numpy>=2.5.3",
|
||||||
"openai>=3.17.0",
|
"openai>=3.17.0",
|
||||||
|
"opencv-python>=5.0.0.93",
|
||||||
"psutil>=7.2.2",
|
"psutil>=7.2.2",
|
||||||
"pyobjc-framework-contacts>=12.2.2",
|
"pyobjc-framework-contacts>=12.2.2",
|
||||||
"pyobjc-framework-eventkit>=12.2.2",
|
"pyobjc-framework-eventkit>=12.2.2",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ exec "{python}" main.py >> "$HOME/Library/Logs/{APP_NAME}.log" 2>&1
|
|||||||
"CFBundleExecutable": APP_NAME, "CFBundleIconFile": f"{APP_NAME}.icns", "LSMinimumSystemVersion": "13.0",
|
"CFBundleExecutable": APP_NAME, "CFBundleIconFile": f"{APP_NAME}.icns", "LSMinimumSystemVersion": "13.0",
|
||||||
"NSHighResolutionCapable": True,
|
"NSHighResolutionCapable": True,
|
||||||
"NSMicrophoneUsageDescription": "Per ascoltare la tua voce.",
|
"NSMicrophoneUsageDescription": "Per ascoltare la tua voce.",
|
||||||
|
"NSCameraUsageDescription": "Per guardare l'ambiente dalla webcam quando lo chiedi.",
|
||||||
"NSCalendarsFullAccessUsageDescription": "Per leggere e creare eventi nel Calendario.",
|
"NSCalendarsFullAccessUsageDescription": "Per leggere e creare eventi nel Calendario.",
|
||||||
"NSCalendarsUsageDescription": "Per leggere e creare eventi nel Calendario.",
|
"NSCalendarsUsageDescription": "Per leggere e creare eventi nel Calendario.",
|
||||||
"NSRemindersFullAccessUsageDescription": "Per gestire i promemoria.",
|
"NSRemindersFullAccessUsageDescription": "Per gestire i promemoria.",
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ dependencies = [
|
|||||||
{ name = "mlx-whisper" },
|
{ name = "mlx-whisper" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
|
{ name = "opencv-python" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
{ name = "pyobjc-framework-contacts" },
|
{ name = "pyobjc-framework-contacts" },
|
||||||
{ name = "pyobjc-framework-eventkit" },
|
{ name = "pyobjc-framework-eventkit" },
|
||||||
@@ -134,6 +135,7 @@ requires-dist = [
|
|||||||
{ name = "mlx-whisper", specifier = ">=0.4.3" },
|
{ name = "mlx-whisper", specifier = ">=0.4.3" },
|
||||||
{ name = "numpy", specifier = ">=2.5.3" },
|
{ name = "numpy", specifier = ">=2.5.3" },
|
||||||
{ name = "openai", specifier = ">=3.17.0" },
|
{ name = "openai", specifier = ">=3.17.0" },
|
||||||
|
{ name = "opencv-python", specifier = ">=5.0.0.93" },
|
||||||
{ name = "psutil", specifier = ">=7.2.2" },
|
{ name = "psutil", specifier = ">=7.2.2" },
|
||||||
{ name = "pyobjc-framework-contacts", specifier = ">=12.2.2" },
|
{ name = "pyobjc-framework-contacts", specifier = ">=12.2.2" },
|
||||||
{ name = "pyobjc-framework-eventkit", specifier = ">=12.2.2" },
|
{ name = "pyobjc-framework-eventkit", specifier = ">=12.2.2" },
|
||||||
@@ -1925,6 +1927,25 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/57/bc/5b80a0ca4069a6b5ba6c7d48ae7a4f8176b2c0bde0bebccdb867472ec6f6/openai-3.17.0-py3-none-any.whl", hash = "sha256:712bc2982a6da1af10f6948a1d2d9ff5ce3ee3604ded88002a53765a77cf5ae5", size = 2068574, upload-time = "2026-09-22T02:37:15.373Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/bc/5b80a0ca4069a6b5ba6c7d48ae7a4f8176b2c0bde0bebccdb867472ec6f6/openai-3.17.0-py3-none-any.whl", hash = "sha256:712bc2982a6da1af10f6948a1d2d9ff5ce3ee3604ded88002a53765a77cf5ae5", size = 2068574, upload-time = "2026-09-22T02:37:15.373Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opencv-python"
|
||||||
|
version = "5.0.0.93"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.3"
|
version = "26.3"
|
||||||
|
|||||||
Reference in new issue
Block a user