#!/usr/bin/env python3 from __future__ import annotations import subprocess import tempfile from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] MEDIA_DIR = ROOT / "docs" / "media" FONT_REGULAR = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" def font(size: int, *, bold: bool = False) -> ImageFont.ImageFont: candidate = FONT_BOLD if bold else FONT_REGULAR try: return ImageFont.truetype(candidate, size=size) except OSError: return ImageFont.load_default() def draw_round_rect(draw: ImageDraw.ImageDraw, box, radius: int, *, fill, outline=None, width=1): draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) def draw_background(size: tuple[int, int], *, light=False) -> Image.Image: w, h = size image = Image.new("RGBA", size, "#0d111b" if not light else "#e5e8ef") draw = ImageDraw.Draw(image) for y in range(h): mix = y / max(1, h - 1) if light: color = ( int(229 + (240 - 229) * mix), int(232 + (241 - 232) * mix), int(239 + (246 - 239) * mix), 255, ) else: color = ( int(13 + (30 - 13) * mix), int(17 + (49 - 17) * mix), int(27 + (79 - 27) * mix), 255, ) draw.line((0, y, w, y), fill=color) draw.ellipse((60, 70, 360, 370), fill=(43, 108, 176, 90)) draw.ellipse((w - 360, h - 340, w - 40, h - 20), fill=(14, 116, 144, 70)) draw.ellipse((w - 260, 40, w - 80, 220), fill=(244, 114, 182, 50)) return image def paste_center(base: Image.Image, overlay: Image.Image, top: int) -> tuple[int, int]: x = (base.width - overlay.width) // 2 base.alpha_composite(overlay, (x, top)) return (x, top) def draw_text_block( draw: ImageDraw.ImageDraw, origin: tuple[int, int], lines: list[str], *, fill, title=None, title_fill=None, line_gap=12, body_font=None, title_font=None, ): x, y = origin title_font = title_font or font(26, bold=True) body_font = body_font or font(22) if title: draw.text((x, y), title, font=title_font, fill=title_fill or fill) y += title_font.size + 10 for line in lines: draw.text((x, y), line, font=body_font, fill=fill) y += body_font.size + line_gap def build_settings_window() -> Image.Image: base = draw_background((1440, 900)) window = Image.new("RGBA", (1180, 760), (248, 250, 252, 255)) draw = ImageDraw.Draw(window) draw_round_rect(draw, (0, 0, 1179, 759), 26, fill="#f8fafc", outline="#cbd5e1", width=2) draw_round_rect(draw, (0, 0, 1179, 74), 26, fill="#182130") draw.rectangle((0, 40, 1179, 74), fill="#182130") draw.text((32, 22), "Aman Settings (Required)", font=font(28, bold=True), fill="#f8fafc") draw.text((970, 24), "Cancel", font=font(20), fill="#cbd5e1") draw_round_rect(draw, (1055, 14, 1146, 58), 16, fill="#0f766e") draw.text((1080, 24), "Apply", font=font(20, bold=True), fill="#f8fafc") draw_round_rect(draw, (26, 94, 1154, 160), 18, fill="#fff7d6", outline="#facc15") draw_text_block( draw, (48, 112), ["Aman needs saved settings before it can start recording from the tray."], fill="#4d3a00", ) draw_round_rect(draw, (26, 188, 268, 734), 20, fill="#eef2f7", outline="#d7dee9") sections = ["General", "Audio", "Runtime & Models", "Help", "About"] y = 224 for index, label in enumerate(sections): active = index == 0 fill = "#dbeafe" if active else "#eef2f7" outline = "#93c5fd" if active else "#eef2f7" draw_round_rect(draw, (46, y, 248, y + 58), 16, fill=fill, outline=outline) draw.text((68, y + 16), label, font=font(22, bold=active), fill="#0f172a") y += 76 draw_round_rect(draw, (300, 188, 1154, 734), 20, fill="#ffffff", outline="#d7dee9") draw_text_block(draw, (332, 220), [], title="General", fill="#0f172a", title_font=font(30, bold=True)) labels = [ ("Trigger hotkey", "Super+m"), ("Text injection", "Clipboard paste (recommended)"), ("Transcription language", "Auto detect"), ("Profile", "Default"), ] y = 286 for label, value in labels: draw.text((332, y), label, font=font(22, bold=True), fill="#0f172a") draw_round_rect(draw, (572, y - 8, 1098, y + 38), 14, fill="#f8fafc", outline="#cbd5e1") draw.text((596, y + 4), value, font=font(20), fill="#334155") y += 92 draw_round_rect(draw, (332, 480, 1098, 612), 18, fill="#f0fdf4", outline="#86efac") draw_text_block( draw, (360, 512), [ "Supported first-run path:", "1. Pick the microphone you want to use.", "2. Keep the recommended clipboard backend.", "3. Click Apply and wait for the tray to return to Idle.", ], fill="#166534", body_font=font(20), ) draw_round_rect(draw, (332, 638, 1098, 702), 18, fill="#e0f2fe", outline="#7dd3fc") draw.text( (360, 660), "After setup, put your cursor in a text field and say: hello from Aman", font=font(20, bold=True), fill="#155e75", ) background = base.copy() paste_center(background, window, 70) return background.convert("RGB") def build_tray_menu() -> Image.Image: base = draw_background((1280, 900), light=True) draw = ImageDraw.Draw(base) draw_round_rect(draw, (0, 0, 1279, 54), 0, fill="#111827") draw.text((42, 16), "X11 Session", font=font(20, bold=True), fill="#e5e7eb") draw_round_rect(draw, (1038, 10, 1180, 42), 14, fill="#1f2937", outline="#374151") draw.text((1068, 17), "Idle", font=font(18, bold=True), fill="#e5e7eb") menu = Image.new("RGBA", (420, 520), (255, 255, 255, 255)) menu_draw = ImageDraw.Draw(menu) draw_round_rect(menu_draw, (0, 0, 419, 519), 22, fill="#ffffff", outline="#cbd5e1", width=2) items = [ "Settings...", "Help", "About", "Pause Aman", "Reload Config", "Run Diagnostics", "Open Config Path", "Quit", ] y = 26 for label in items: highlighted = label == "Run Diagnostics" if highlighted: draw_round_rect(menu_draw, (16, y - 6, 404, y + 40), 14, fill="#dbeafe") menu_draw.text((34, y), label, font=font(22, bold=highlighted), fill="#0f172a") y += 58 if label in {"About", "Run Diagnostics"}: menu_draw.line((24, y - 10, 396, y - 10), fill="#e2e8f0", width=2) paste_center(base, menu, 118) return base.convert("RGB") def build_terminal_scene() -> Image.Image: image = Image.new("RGB", (1280, 720), "#0b1220") draw = ImageDraw.Draw(image) draw_round_rect(draw, (100, 80, 1180, 640), 24, fill="#0f172a", outline="#334155", width=2) draw_round_rect(draw, (100, 80, 1180, 132), 24, fill="#111827") draw.rectangle((100, 112, 1180, 132), fill="#111827") draw.text((136, 97), "Terminal", font=font(26, bold=True), fill="#e2e8f0") draw.text((168, 192), "$ sha256sum -c aman-x11-linux-0.1.0.tar.gz.sha256", font=font(22), fill="#86efac") draw.text((168, 244), "aman-x11-linux-0.1.0.tar.gz: OK", font=font(22), fill="#cbd5e1") draw.text((168, 310), "$ tar -xzf aman-x11-linux-0.1.0.tar.gz", font=font(22), fill="#86efac") draw.text((168, 362), "$ cd aman-x11-linux-0.1.0", font=font(22), fill="#86efac") draw.text((168, 414), "$ ./install.sh", font=font(22), fill="#86efac") draw.text((168, 482), "Installed aman.service and started the user service.", font=font(22), fill="#cbd5e1") draw.text((168, 534), "Waiting for first-run settings...", font=font(22), fill="#7dd3fc") draw.text((128, 30), "1. Install the portable bundle", font=font(34, bold=True), fill="#f8fafc") return image def build_editor_scene(*, badge: str | None = None, text: str = "", subtitle: str) -> Image.Image: image = draw_background((1280, 720), light=True).convert("RGB") draw = ImageDraw.Draw(image) draw_round_rect(draw, (84, 64, 1196, 642), 26, fill="#ffffff", outline="#cbd5e1", width=2) draw_round_rect(draw, (84, 64, 1196, 122), 26, fill="#f8fafc") draw.rectangle((84, 94, 1196, 122), fill="#f8fafc") draw.text((122, 84), "Focused editor", font=font(24, bold=True), fill="#0f172a") draw.text((122, 158), subtitle, font=font(26, bold=True), fill="#0f172a") draw_round_rect(draw, (996, 80, 1144, 116), 16, fill="#111827") draw.text((1042, 89), "Idle", font=font(18, bold=True), fill="#e5e7eb") if badge: fill = {"Recording": "#dc2626", "STT": "#2563eb", "AI Processing": "#0f766e"}[badge] draw_round_rect(draw, (122, 214, 370, 262), 18, fill=fill) draw.text((150, 225), badge, font=font(24, bold=True), fill="#f8fafc") draw_round_rect(draw, (122, 308, 1158, 572), 22, fill="#f8fafc", outline="#d7dee9") if text: draw.multiline_text((156, 350), text, font=font(34), fill="#0f172a", spacing=18) else: draw.text((156, 366), "Cursor ready for dictation...", font=font(32), fill="#64748b") return image def build_demo_webm(settings_png: Path, tray_png: Path, output: Path) -> None: scenes = [ ("01-install.png", build_terminal_scene(), 3.0), ("02-settings.png", Image.open(settings_png).resize((1280, 800)).crop((0, 40, 1280, 760)), 4.0), ("03-tray.png", Image.open(tray_png).resize((1280, 900)).crop((0, 90, 1280, 810)), 3.0), ( "04-editor-ready.png", build_editor_scene( subtitle="2. Press the hotkey and say: hello from Aman", text="", ), 3.0, ), ( "05-recording.png", build_editor_scene( badge="Recording", subtitle="Tray and status now show recording", text="", ), 1.5, ), ( "06-stt.png", build_editor_scene( badge="STT", subtitle="Aman transcribes the audio locally", text="", ), 1.5, ), ( "07-processing.png", build_editor_scene( badge="AI Processing", subtitle="Cleanup and injection finish automatically", text="", ), 1.5, ), ( "08-result.png", build_editor_scene( subtitle="3. The text lands in the focused app", text="Hello from Aman.", ), 4.0, ), ] with tempfile.TemporaryDirectory() as td: temp_dir = Path(td) concat = temp_dir / "scenes.txt" concat_lines: list[str] = [] for name, image, duration in scenes: frame_path = temp_dir / name image.convert("RGB").save(frame_path, format="PNG") concat_lines.append(f"file '{frame_path.as_posix()}'") concat_lines.append(f"duration {duration}") concat_lines.append(f"file '{(temp_dir / scenes[-1][0]).as_posix()}'") concat.write_text("\n".join(concat_lines) + "\n", encoding="utf-8") subprocess.run( [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(concat), "-vf", "fps=24,format=yuv420p", "-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "34", str(output), ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def main() -> None: MEDIA_DIR.mkdir(parents=True, exist_ok=True) settings_png = MEDIA_DIR / "settings-window.png" tray_png = MEDIA_DIR / "tray-menu.png" demo_webm = MEDIA_DIR / "first-run-demo.webm" build_settings_window().save(settings_png, format="PNG") build_tray_menu().save(tray_png, format="PNG") build_demo_webm(settings_png, tray_png, demo_webm) print(f"wrote {settings_png}") print(f"wrote {tray_png}") print(f"wrote {demo_webm}") if __name__ == "__main__": main()