Changes
This commit is contained in:
BIN
assets/face_wire.png
Normal file
BIN
assets/face_wire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
281
display.py
Normal file
281
display.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
JARVIS Display
|
||||||
|
──────────────
|
||||||
|
Renders the JARVIS UI overlay. Works in two modes:
|
||||||
|
|
||||||
|
X11 (development laptop):
|
||||||
|
python display.py
|
||||||
|
|
||||||
|
Linux framebuffer (device – no windowing system):
|
||||||
|
python display.py --framebuffer
|
||||||
|
# or
|
||||||
|
JARVIS_FB=1 python display.py
|
||||||
|
|
||||||
|
Framebuffer prerequisites on Debian (device):
|
||||||
|
sudo apt install python3-pygame
|
||||||
|
sudo usermod -aG video $USER # then log out/in
|
||||||
|
# If fbdev fails (common on newer kernels) try KMS/DRM:
|
||||||
|
SDL_VIDEODRIVER=kmsdrm python display.py --framebuffer
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ── CLI / env-var flags ─────────────────────────────────────────────────────
|
||||||
|
# These must be parsed BEFORE pygame is imported so we can set SDL env vars
|
||||||
|
# in time for pygame.init() to pick them up.
|
||||||
|
|
||||||
|
_parser = argparse.ArgumentParser(description='JARVIS display', add_help=True)
|
||||||
|
_parser.add_argument(
|
||||||
|
'--framebuffer', '-fb', action='store_true',
|
||||||
|
help='Render to Linux framebuffer instead of an X11/Wayland window')
|
||||||
|
_parser.add_argument('--width', type=int, default=1280,
|
||||||
|
help='Screen / framebuffer width (default 1280)')
|
||||||
|
_parser.add_argument('--height', type=int, default=800,
|
||||||
|
help='Screen / framebuffer height (default 800)')
|
||||||
|
_args, _ = _parser.parse_known_args()
|
||||||
|
|
||||||
|
USE_FB: bool = _args.framebuffer or bool(os.environ.get('JARVIS_FB'))
|
||||||
|
|
||||||
|
if USE_FB:
|
||||||
|
# SDL2 env vars must be set before pygame.display.init()
|
||||||
|
os.environ.setdefault('SDL_VIDEODRIVER', 'fbdev')
|
||||||
|
os.environ.setdefault('SDL_FBDEV', os.environ.get('JARVIS_FBDEV', '/dev/fb0'))
|
||||||
|
# Prevent SDL trying to open a mouse device on the framebuffer
|
||||||
|
os.environ.setdefault('SDL_NOMOUSE', '1')
|
||||||
|
|
||||||
|
import pygame # noqa: E402 — import after env setup
|
||||||
|
|
||||||
|
# ── Colour palette ───────────────────────────────────────────────────────────
|
||||||
|
BLACK = ( 0, 0, 0)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
GRAY = (160, 160, 160)
|
||||||
|
DIM_GRAY = ( 80, 80, 80)
|
||||||
|
WIRE_COL = ( 58, 58, 58) # subdued wireframe line colour
|
||||||
|
|
||||||
|
# ── Wireframe face ────────────────────────────────────────────────────────────
|
||||||
|
# Pure 2-D face drawing. Face shape is defined by an explicit width profile;
|
||||||
|
# eye sockets, nose bridge and chin are drawn as distinct features so the
|
||||||
|
# brain immediately reads "face" rather than a geometric solid.
|
||||||
|
|
||||||
|
WIRE_EYE = ( 95, 95, 95) # slightly brighter for eye/feature lines
|
||||||
|
|
||||||
|
def _hw(t: float, rx: float) -> float:
|
||||||
|
"""
|
||||||
|
Face half-width in pixels at normalised height t.
|
||||||
|
t = -1 → crown (top)
|
||||||
|
t = 0 → cheek level (widest, ~= rx)
|
||||||
|
t = +1 → chin tip
|
||||||
|
Upper half: rounded ellipse.
|
||||||
|
Lower half: faster taper toward chin.
|
||||||
|
"""
|
||||||
|
if t <= 0.0:
|
||||||
|
return rx * math.sqrt(max(0.0, 1.0 - t * t))
|
||||||
|
else:
|
||||||
|
return rx * math.sqrt(max(0.0, 1.0 - t * t)) * (1.0 - 0.30 * t)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_wireframe_face(surface: pygame.Surface,
|
||||||
|
cx: int, cy: int,
|
||||||
|
rx: float, ry: float) -> None:
|
||||||
|
"""
|
||||||
|
Draw a 2-D face wireframe centred at (cx, cy).
|
||||||
|
Coordinate system: t ∈ [−1, +1] (−1=crown, 0=cheeks, +1=chin)
|
||||||
|
x_fraction ∈ [−1, +1] (fraction of half-width at t)
|
||||||
|
"""
|
||||||
|
N_H = 13 # horizontal grid lines
|
||||||
|
N_V = 11 # vertical grid lines (including edges)
|
||||||
|
PTS = 40 # interpolation steps per curve
|
||||||
|
|
||||||
|
# Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
def sc(xf: float, t: float) -> tuple[int, int]:
|
||||||
|
"""xf (fraction of half-width) + height t → screen pixel."""
|
||||||
|
w = _hw(t, rx)
|
||||||
|
# Slight forward bow: face curves toward viewer at cheek level
|
||||||
|
bow = int(0.06 * w * (1.0 - t * t) * (1.0 - xf * xf))
|
||||||
|
return (int(cx + xf * w), int(cy + t * ry) - bow)
|
||||||
|
|
||||||
|
def draw(pts, close=False, col=WIRE_COL):
|
||||||
|
if len(pts) >= 2:
|
||||||
|
pygame.draw.lines(surface, col, close, pts, 1)
|
||||||
|
|
||||||
|
# Face outline (left and right silhouette) ────────────────────────────────
|
||||||
|
t_vals = [(-1.0 + 2.0 * i / (PTS - 1)) for i in range(PTS)]
|
||||||
|
draw([sc(-1.0, t) for t in t_vals]) # left edge
|
||||||
|
draw([sc( 1.0, t) for t in t_vals]) # right edge
|
||||||
|
# Crown arc (straight line across the very top)
|
||||||
|
draw([sc(-1.0, -1.0), sc(1.0, -1.0)])
|
||||||
|
# Chin arc
|
||||||
|
draw([sc(-0.6, 1.0), sc(0.0, 1.02), sc(0.6, 1.0)])
|
||||||
|
|
||||||
|
# Horizontal grid lines ───────────────────────────────────────────────────
|
||||||
|
for i in range(1, N_H):
|
||||||
|
t = -1.0 + 2.0 * i / N_H
|
||||||
|
draw([sc(-1.0 + 2.0 * j / (PTS - 1), t) for j in range(PTS)])
|
||||||
|
|
||||||
|
# Vertical grid lines ─────────────────────────────────────────────────────
|
||||||
|
for i in range(1, N_V):
|
||||||
|
xf = -1.0 + 2.0 * i / N_V
|
||||||
|
draw([sc(xf, t) for t in t_vals])
|
||||||
|
|
||||||
|
# ── Eye sockets ───────────────────────────────────────────────────────────
|
||||||
|
# Each eye is a small ellipse drawn in a slightly brighter colour so it
|
||||||
|
# immediately reads as a facial feature.
|
||||||
|
eye_t = -0.38 # height: upper face (~35% from crown)
|
||||||
|
eye_xf = 0.35 # lateral offset (fraction of half-width at eye_t)
|
||||||
|
eye_w = _hw(eye_t, rx) # face half-width at eye level
|
||||||
|
e_rx = int(eye_w * 0.24)
|
||||||
|
e_ry = int(ry * 0.09)
|
||||||
|
|
||||||
|
for sign in (-1, +1):
|
||||||
|
ecx = int(cx + sign * eye_xf * eye_w)
|
||||||
|
ecy = int(cy + eye_t * ry)
|
||||||
|
eye_pts = [
|
||||||
|
(int(ecx + e_rx * math.cos(2 * math.pi * k / 28)),
|
||||||
|
int(ecy + e_ry * math.sin(2 * math.pi * k / 28)))
|
||||||
|
for k in range(28)
|
||||||
|
]
|
||||||
|
draw(eye_pts + [eye_pts[0]], col=WIRE_EYE)
|
||||||
|
|
||||||
|
# ── Nose bridge ───────────────────────────────────────────────────────────
|
||||||
|
# Two short lines running from inner eye corners down to nose tip
|
||||||
|
inner = int(eye_w * 0.12)
|
||||||
|
e_btm = int(cy + eye_t * ry) + e_ry
|
||||||
|
nose_y = int(cy + 0.08 * ry)
|
||||||
|
nose_w = int(eye_w * 0.07)
|
||||||
|
draw([(cx - inner, e_btm), (cx - nose_w, nose_y)], col=WIRE_COL)
|
||||||
|
draw([(cx + inner, e_btm), (cx + nose_w, nose_y)], col=WIRE_COL)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Clock / date ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def draw_clock(surface: pygame.Surface, fonts: dict,
|
||||||
|
x: int, y: int) -> None:
|
||||||
|
now = datetime.now()
|
||||||
|
date_str = now.strftime('%A, %B %-d, %Y')
|
||||||
|
time_str = now.strftime('%-H:%M')
|
||||||
|
secs_str = now.strftime('%S')
|
||||||
|
|
||||||
|
# Date row
|
||||||
|
surface.blit(fonts['small'].render(date_str, True, GRAY), (x, y))
|
||||||
|
|
||||||
|
# Large time + superscript seconds
|
||||||
|
t_surf = fonts['large'].render(time_str, True, WHITE)
|
||||||
|
s_surf = fonts['small'].render(secs_str, True, DIM_GRAY)
|
||||||
|
ty = y + fonts['small'].get_height() + 4
|
||||||
|
surface.blit(t_surf, (x, ty))
|
||||||
|
surface.blit(s_surf, (x + t_surf.get_width() + 2,
|
||||||
|
ty + t_surf.get_height() - s_surf.get_height() - 8))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Status line ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def draw_status(surface: pygame.Surface, fonts: dict,
|
||||||
|
text: str, cx: int, y: int) -> None:
|
||||||
|
surf = fonts['small'].render(text, True, GRAY)
|
||||||
|
surface.blit(surf, (cx - surf.get_width() // 2, y))
|
||||||
|
|
||||||
|
|
||||||
|
def draw_dot(surface: pygame.Surface, cx: int, y: int,
|
||||||
|
radius: int = 9) -> None:
|
||||||
|
pygame.draw.circle(surface, WHITE, (cx, y), radius)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Font helper ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_font(size: int, bold: bool = False) -> pygame.font.Font:
|
||||||
|
"""Try a list of clean system fonts, fall back to pygame built-in."""
|
||||||
|
for name in ('DejaVuSans', 'FreeSans', 'LiberationSans',
|
||||||
|
'Helvetica', 'Arial', None):
|
||||||
|
try:
|
||||||
|
f = pygame.font.SysFont(name, size, bold=bold)
|
||||||
|
if f:
|
||||||
|
return f
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return pygame.font.Font(None, size)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pygame.init()
|
||||||
|
pygame.mouse.set_visible(False)
|
||||||
|
|
||||||
|
W, H = _args.width, _args.height
|
||||||
|
|
||||||
|
if USE_FB:
|
||||||
|
screen = pygame.display.set_mode((W, H), pygame.FULLSCREEN | pygame.NOFRAME)
|
||||||
|
else:
|
||||||
|
screen = pygame.display.set_mode((W, H))
|
||||||
|
pygame.display.set_caption('JARVIS')
|
||||||
|
|
||||||
|
fonts = {
|
||||||
|
'large': _load_font(76),
|
||||||
|
'small': _load_font(24),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Layout anchors — all relative to screen size so they adapt to resolution
|
||||||
|
face_cx = W // 2
|
||||||
|
face_cy = int(H * 0.31)
|
||||||
|
face_rx = int(min(W, H) * 0.150) # half-width at cheek level
|
||||||
|
face_ry = int(min(W, H) * 0.175) # half-height crown→chin
|
||||||
|
status_y = int(H * 0.82)
|
||||||
|
dot_y = int(H * 0.895)
|
||||||
|
|
||||||
|
# ── Load face wireframe PNG if present ────────────────────────────────────
|
||||||
|
# Place any face wireframe PNG at assets/face_wire.png and it will be
|
||||||
|
# used automatically. Black background is composited away via BLEND_ADD
|
||||||
|
# so only the bright lines show. Falls back to parametric drawing.
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_png_path = os.path.join(_here, 'assets', 'face_wire.png')
|
||||||
|
face_img = None
|
||||||
|
if os.path.exists(_png_path):
|
||||||
|
raw = pygame.image.load(_png_path).convert()
|
||||||
|
img_src_w, img_src_h = raw.get_size()
|
||||||
|
# Scale to fit face slot while preserving aspect ratio
|
||||||
|
scale = min((face_rx * 2.2) / img_src_w,
|
||||||
|
(face_ry * 2.4) / img_src_h)
|
||||||
|
img_w = max(1, int(img_src_w * scale))
|
||||||
|
img_h = max(1, int(img_src_h * scale))
|
||||||
|
face_img = pygame.transform.smoothscale(raw, (img_w, img_h))
|
||||||
|
|
||||||
|
status_text = 'Initializing...'
|
||||||
|
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
return
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key in (pygame.K_ESCAPE, pygame.K_q):
|
||||||
|
pygame.quit()
|
||||||
|
return
|
||||||
|
# Touch / mouse tap anywhere also quits in kiosk mode
|
||||||
|
if USE_FB and event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
pygame.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
screen.fill(BLACK)
|
||||||
|
if face_img:
|
||||||
|
screen.blit(face_img,
|
||||||
|
face_img.get_rect(center=(face_cx, face_cy)),
|
||||||
|
special_flags=pygame.BLEND_ADD)
|
||||||
|
else:
|
||||||
|
draw_wireframe_face(screen, face_cx, face_cy, face_rx, face_ry)
|
||||||
|
draw_clock(screen, fonts, 20, 14)
|
||||||
|
draw_status(screen, fonts, status_text, W // 2, status_y)
|
||||||
|
draw_dot(screen, W // 2, dot_y)
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(10) # 10 fps is plenty for a status screen
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
118
gen_face.py
Normal file
118
gen_face.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
gen_face.py – Run ONCE to generate assets/face_wire.png
|
||||||
|
─────────────────────────────────────────────────────────
|
||||||
|
Uses matplotlib's 3-D surface renderer to produce a wireframe face image
|
||||||
|
that display.py loads. Run it once; the PNG is then part of your project.
|
||||||
|
|
||||||
|
python gen_face.py
|
||||||
|
|
||||||
|
Dependencies (dev machine only – NOT needed on the target device):
|
||||||
|
sudo apt install python3-matplotlib # or: pip install matplotlib
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg') # off-screen — no display needed
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D # noqa: F401
|
||||||
|
|
||||||
|
# ── Face width profile ────────────────────────────────────────────────────────
|
||||||
|
# (normalised_height, width_fraction) 0 = crown, 1 = chin
|
||||||
|
# Widest point is at the cheekbones (~t=0.37), not the crown.
|
||||||
|
# Crown and forehead are slightly narrower; jaw/chin taper below cheeks.
|
||||||
|
_PROFILE = [
|
||||||
|
(0.00, 0.82), # crown
|
||||||
|
(0.10, 0.88), # upper skull
|
||||||
|
(0.22, 0.94), # temples / forehead
|
||||||
|
(0.37, 1.00), # cheekbones — widest
|
||||||
|
(0.50, 0.96), # nose level
|
||||||
|
(0.62, 0.84), # mouth level
|
||||||
|
(0.73, 0.68), # jaw angle
|
||||||
|
(0.83, 0.50), # lower jaw
|
||||||
|
(0.92, 0.30), # chin
|
||||||
|
(1.00, 0.08), # chin tip
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _face_r(v: float) -> float:
|
||||||
|
"""Radial width at normalised height v ∈ [0, 1]."""
|
||||||
|
for i in range(len(_PROFILE) - 1):
|
||||||
|
t0, w0 = _PROFILE[i]
|
||||||
|
t1, w1 = _PROFILE[i + 1]
|
||||||
|
if t0 <= v <= t1:
|
||||||
|
return w0 + (v - t0) / (t1 - t0) * (w1 - w0)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def face_surface(nu: int = 22, nv: int = 28):
|
||||||
|
"""
|
||||||
|
Build the face mesh arrays.
|
||||||
|
Axis convention chosen to match matplotlib's default camera behaviour:
|
||||||
|
X = left / right
|
||||||
|
Y = depth (positive = toward viewer / front of face)
|
||||||
|
Z = up / down (+1 = crown, −1 = chin)
|
||||||
|
To view from the front: view_init(elev, azim=90) ← camera is at +Y
|
||||||
|
"""
|
||||||
|
u_vals = np.linspace(-math.pi / 2, math.pi / 2, nu) # front hemisphere
|
||||||
|
v_vals = np.linspace(0.0, 1.0, nv) # crown → chin
|
||||||
|
|
||||||
|
X = np.zeros((nv, nu))
|
||||||
|
Y = np.zeros((nv, nu))
|
||||||
|
Z = np.zeros((nv, nu))
|
||||||
|
|
||||||
|
for j, v in enumerate(v_vals):
|
||||||
|
r = _face_r(v)
|
||||||
|
z = math.cos(math.pi * v) # +1 at crown, −1 at chin
|
||||||
|
for i, u in enumerate(u_vals):
|
||||||
|
X[j, i] = r * math.sin(u) # left/right
|
||||||
|
Y[j, i] = r * math.cos(u) # depth (front = max)
|
||||||
|
Z[j, i] = z # height
|
||||||
|
|
||||||
|
return X, Y, Z
|
||||||
|
|
||||||
|
|
||||||
|
def generate(out_path: str = 'assets/face_wire.png',
|
||||||
|
img_w: int = 400, img_h: int = 500,
|
||||||
|
line_col: str = '#606060') -> None:
|
||||||
|
dpi = 100
|
||||||
|
fig = plt.figure(figsize=(img_w / dpi, img_h / dpi), facecolor='black')
|
||||||
|
ax = fig.add_subplot(111, projection='3d', facecolor='black')
|
||||||
|
|
||||||
|
X, Y, Z = face_surface(nu=18, nv=22)
|
||||||
|
ax.plot_wireframe(X, Y, Z,
|
||||||
|
color=line_col,
|
||||||
|
linewidth=0.85,
|
||||||
|
rstride=1, cstride=1)
|
||||||
|
|
||||||
|
# azim=90 → camera at +Y, looking toward −Y (directly at face)
|
||||||
|
# elev=-8 → camera slightly below horizontal = view from below,
|
||||||
|
# shows chin/jaw, hides flat top of mesh — matches reference
|
||||||
|
ax.view_init(elev=-8, azim=90)
|
||||||
|
|
||||||
|
ax.set_axis_off()
|
||||||
|
# [x_width, y_depth, z_height]
|
||||||
|
# Key: y_depth very small → face is flat/mask-like, not a globe
|
||||||
|
ax.set_box_aspect([1.0, 0.28, 1.22])
|
||||||
|
|
||||||
|
plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(out_path) if os.path.dirname(out_path) else '.',
|
||||||
|
exist_ok=True)
|
||||||
|
fig.savefig(out_path, dpi=dpi, facecolor='black',
|
||||||
|
bbox_inches='tight', pad_inches=0.02)
|
||||||
|
plt.close(fig)
|
||||||
|
print(f'Saved {out_path} ({img_w}×{img_h} px)')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--out', default='assets/face_wire.png')
|
||||||
|
ap.add_argument('--width', type=int, default=400)
|
||||||
|
ap.add_argument('--height', type=int, default=500)
|
||||||
|
ap.add_argument('--color', default='#606060')
|
||||||
|
args = ap.parse_args()
|
||||||
|
generate(args.out, args.width, args.height, args.color)
|
||||||
106
gen_face_ai.py
Normal file
106
gen_face_ai.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
gen_face_ai.py – Generate assets/face_wire.png using Gemini image generation
|
||||||
|
────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Uses the Gemini / Google Gen AI SDK with a native image-generation model
|
||||||
|
(gemini-2.5-flash-image or similar) to produce a wireframe face PNG ready for
|
||||||
|
display.py to load.
|
||||||
|
|
||||||
|
pip install google-genai pillow
|
||||||
|
python3 gen_face_ai.py
|
||||||
|
|
||||||
|
Or set the key in the environment:
|
||||||
|
export GEMINI_API_KEY=YOUR_KEY
|
||||||
|
python3 gen_face_ai.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# ── Paste your Gemini API key here ────────────────────────────────────────────
|
||||||
|
API_KEY = 'AQ.Ab8RN6LuGwkGiKPa61jsLAEYEpJp1Yl2EkZuBWTbN9AMKxgTSw'
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── CLI ───────────────────────────────────────────────────────────────────────
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--key', default='', help='Override the hardcoded API key')
|
||||||
|
ap.add_argument('--out', default='assets/face_wire.png')
|
||||||
|
ap.add_argument('--model', default='gemini-2.5-flash-image',
|
||||||
|
help='Gemini image model to use (default: gemini-2.5-flash-image)')
|
||||||
|
ap.add_argument('--list-models', action='store_true',
|
||||||
|
help='Print models that support generateContent then exit')
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
api_key = args.key or API_KEY or os.environ.get('GEMINI_API_KEY', '')
|
||||||
|
if not api_key:
|
||||||
|
sys.exit('ERROR: paste your key into API_KEY at the top of this file')
|
||||||
|
|
||||||
|
# ── Install check ─────────────────────────────────────────────────────────────
|
||||||
|
try:
|
||||||
|
from google import genai
|
||||||
|
from google.genai import types
|
||||||
|
except ImportError:
|
||||||
|
sys.exit('Run: pip install google-genai then try again.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
sys.exit('Run: pip install pillow then try again.')
|
||||||
|
|
||||||
|
# ── Connect ───────────────────────────────────────────────────────────────────
|
||||||
|
print('Connecting to Google GenAI …')
|
||||||
|
client = genai.Client(api_key=api_key)
|
||||||
|
|
||||||
|
if args.list_models:
|
||||||
|
print('\nModels with generateContent support:')
|
||||||
|
for m in client.models.list():
|
||||||
|
methods = getattr(m, 'supported_actions', None) or getattr(m, 'supported_methods', None) or []
|
||||||
|
if 'generateContent' in methods:
|
||||||
|
print(f' {m.name}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ── Prompt ────────────────────────────────────────────────────────────────────
|
||||||
|
PROMPT = (
|
||||||
|
"3D wireframe polygon mesh of a human head and face, viewed from slightly "
|
||||||
|
"below, front-facing, neutral expression, pure black background, thin "
|
||||||
|
"light gray lines only forming a grid over the head and face, no colour "
|
||||||
|
"fill, no skin texture, no neck, symmetrical, sci-fi holographic display "
|
||||||
|
"style, high contrast monochrome"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Generate ──────────────────────────────────────────────────────────────────
|
||||||
|
print(f'Generating with {args.model} …')
|
||||||
|
response = client.models.generate_content(
|
||||||
|
model = args.model,
|
||||||
|
contents= PROMPT,
|
||||||
|
config = types.GenerateContentConfig(
|
||||||
|
response_modalities = ['IMAGE', 'TEXT'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Extract image bytes ───────────────────────────────────────────────────────
|
||||||
|
img_bytes = None
|
||||||
|
for part in response.candidates[0].content.parts:
|
||||||
|
if part.inline_data and part.inline_data.mime_type.startswith('image/'):
|
||||||
|
img_bytes = part.inline_data.data
|
||||||
|
break
|
||||||
|
|
||||||
|
if img_bytes is None:
|
||||||
|
# Print any text the model returned to help debug
|
||||||
|
for part in response.candidates[0].content.parts:
|
||||||
|
if hasattr(part, 'text') and part.text:
|
||||||
|
print('Model text response:', part.text[:400])
|
||||||
|
sys.exit('No image in response – try a different --model')
|
||||||
|
|
||||||
|
# ── Save as PNG ───────────────────────────────────────────────────────────────
|
||||||
|
os.makedirs(os.path.dirname(args.out) if os.path.dirname(args.out) else '.',
|
||||||
|
exist_ok=True)
|
||||||
|
|
||||||
|
# Convert whatever format came back to a proper PNG
|
||||||
|
img = Image.open(io.BytesIO(img_bytes))
|
||||||
|
img.save(args.out, 'PNG')
|
||||||
|
|
||||||
|
print(f'Saved {args.out} ({img.width}×{img.height} px)')
|
||||||
|
print('Run python3 display.py to see it.')
|
||||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# JARVIS dependencies
|
||||||
|
# Install on both dev laptop and device:
|
||||||
|
# sudo apt install python3-pygame python3-pvporcupine python3-pvrecorder
|
||||||
|
# or:
|
||||||
|
# pip install pygame pvporcupine pvrecorder (add --break-system-packages on Debian 13)
|
||||||
|
|
||||||
|
pygame>=2.1.0
|
||||||
|
pvporcupine
|
||||||
|
pvrecorder
|
||||||
Reference in New Issue
Block a user