The website's demo video comes from a script
scripts/video.py runs the demo mesh headless in the terminal set of the screenshots, types a tour of less than two minutes, and records the screen 30 times a second. It paints each different screen in parallel, encodes the video with ffmpeg, and writes demo.mp4, its poster, and its play card into the website checkout's src/. The camera stops while the tour waits for something slow, so the video jumps over the wait. screenshots.py gives the video its app runner, its environment, and an RGB painter. The terminal screenshots stay pixel-identical. The glossary gains "video frame". Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37b17d8ba4
commit
979e31e6fd
4 files changed
+766
-37
No files matched your search
+80
-37
@@ -98,35 +98,15 @@ def main(argv: list[str]) -> int:
|
||||
args = parser.parse_args(argv)
|
||||
if args.child:
|
||||
return _child(args.child, args.out)
|
||||
from meshterm.emulator.font import font_dir
|
||||
|
||||
# The emulator finds its font under the config directory. Each run has a config
|
||||
# directory of its own, so it gets the font directory of this machine by name.
|
||||
fonts = os.environ.get("MESHTERM_EMULATOR_FONTS") or str(font_dir())
|
||||
status = 0
|
||||
if SPLASH in args.only:
|
||||
for path in _draw_splash(args.out / SPLASH):
|
||||
print(f"{SPLASH}: {path}", flush=True)
|
||||
for name in (name for name in args.only if name in SETS):
|
||||
home = Path(tempfile.gettempdir()) / "meshterm-screenshots" / name
|
||||
env = {
|
||||
**os.environ,
|
||||
"MESHTERM_HOME": str(home),
|
||||
"MESHTERM_EMULATOR_FONTS": fonts,
|
||||
"TZ": TZ,
|
||||
"MESHTERM_POWERLINE": "full",
|
||||
"MESHTERM_EMOJI": "1",
|
||||
# The two wraps of a real terminal (column pins, and the last column of a
|
||||
# Windows console) are for a terminal that measures glyphs in its own way.
|
||||
# Here the VT parser measures them as MeshTerm does.
|
||||
"MESHTERM_COLUMN_SNAP": "0",
|
||||
"MESHTERM_FULL_WIDTH": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
}
|
||||
print(f"{name}: capturing...", flush=True)
|
||||
done = subprocess.run(
|
||||
[sys.executable, __file__, "--child", name, "--out", str(args.out)],
|
||||
env=env,
|
||||
env=child_env(Path(tempfile.gettempdir()) / "meshterm-screenshots" / name),
|
||||
check=False,
|
||||
)
|
||||
if done.returncode:
|
||||
@@ -135,6 +115,34 @@ def main(argv: list[str]) -> int:
|
||||
return status
|
||||
|
||||
|
||||
def child_env(home: Path) -> dict[str, str]:
|
||||
"""The environment of a child process that runs MeshTerm for a capture.
|
||||
|
||||
Args:
|
||||
home: The MeshTerm directory of the run (``MESHTERM_HOME``). The demo writes its
|
||||
own directory under it again at each start, and keeps its tile cache there.
|
||||
"""
|
||||
from meshterm.emulator.font import font_dir
|
||||
|
||||
# The emulator finds its font under the config directory. Each run has a config
|
||||
# directory of its own, so it gets the font directory of this machine by name.
|
||||
fonts = os.environ.get("MESHTERM_EMULATOR_FONTS") or str(font_dir())
|
||||
return {
|
||||
**os.environ,
|
||||
"MESHTERM_HOME": str(home),
|
||||
"MESHTERM_EMULATOR_FONTS": fonts,
|
||||
"TZ": TZ,
|
||||
"MESHTERM_POWERLINE": "full",
|
||||
"MESHTERM_EMOJI": "1",
|
||||
# The two wraps of a real terminal (column pins, and the last column of a
|
||||
# Windows console) are for a terminal that measures glyphs in its own way.
|
||||
# Here the VT parser measures them as MeshTerm does.
|
||||
"MESHTERM_COLUMN_SNAP": "0",
|
||||
"MESHTERM_FULL_WIDTH": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- the child
|
||||
|
||||
|
||||
@@ -195,8 +203,43 @@ class Capture:
|
||||
return [list(row) for row in self.terminal.screen]
|
||||
|
||||
|
||||
#: A tour: it gets the capture and a function that types text into MeshTerm.
|
||||
Tour = Callable[[Capture, Callable[[str], None]], None]
|
||||
|
||||
|
||||
def _child(name: str, out: Path) -> int:
|
||||
"""Capture one set in this process. The parent set the environment."""
|
||||
target = out / name
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def tour(capture: Capture, type_text: Callable[[str], None]) -> None:
|
||||
def save(screen: str) -> None:
|
||||
path = target / f"{screen}.png"
|
||||
image = _draw(name, capture.grid(), capture.terminal.palette)
|
||||
image.save(path, optimize=True)
|
||||
print(f"{name}: {path.relative_to(ROOT) if path.is_relative_to(ROOT) else path}")
|
||||
|
||||
_tour(capture, type_text, save)
|
||||
|
||||
return run_demo(name, tour, background=False)
|
||||
|
||||
|
||||
def run_demo(name: str, tour: Tour, *, background: bool) -> int:
|
||||
"""Run MeshTerm with ``--demo`` in this process, as the set ``name``, and do ``tour``.
|
||||
|
||||
The tour runs in a thread of its own while MeshTerm runs in the main thread. When the
|
||||
tour ends, it stops the whole process: exit status 0 if it completed, 1 if it raised
|
||||
an exception. The parent process set the environment (refer to :func:`child_env`).
|
||||
|
||||
Args:
|
||||
name: One of :data:`SETS`.
|
||||
tour: What to do with MeshTerm.
|
||||
background: Whether the demo device plays the background traffic of the world
|
||||
(:data:`meshterm.demo.device.PLAY_BACKGROUND`).
|
||||
|
||||
Returns:
|
||||
1, which comes only if MeshTerm stopped before the tour ended it.
|
||||
"""
|
||||
import time_machine
|
||||
|
||||
traveller = time_machine.travel(NOW, tick=True)
|
||||
@@ -212,7 +255,7 @@ def _child(name: str, out: Path) -> int:
|
||||
from meshterm.emulator.run import _palette, panel_output
|
||||
from meshterm.emulator.vt import Terminal
|
||||
|
||||
demo_device.PLAY_BACKGROUND = False
|
||||
demo_device.PLAY_BACKGROUND = background
|
||||
handheld = DEVICES.get(name)
|
||||
if handheld is not None:
|
||||
cols, rows = handheld.platform.readable_cols, handheld.platform.readable_rows
|
||||
@@ -220,22 +263,14 @@ def _child(name: str, out: Path) -> int:
|
||||
else:
|
||||
cols, rows = TERMINAL_GRID
|
||||
depth, platform = ColorDepth.TRUE_COLOR, "regular"
|
||||
target = out / name
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
lock = threading.Lock()
|
||||
with create_pipe_input() as pipe:
|
||||
terminal = Terminal(cols, rows, palette=_palette(), reply=pipe.send_text)
|
||||
capture = Capture(terminal, lock)
|
||||
|
||||
def save(screen: str) -> None:
|
||||
path = target / f"{screen}.png"
|
||||
image = _draw(name, capture.grid(), terminal.palette)
|
||||
image.save(path, optimize=True)
|
||||
print(f"{name}: {path.relative_to(ROOT) if path.is_relative_to(ROOT) else path}")
|
||||
|
||||
def drive() -> None:
|
||||
try:
|
||||
_tour(capture, pipe.send_text, save)
|
||||
tour(capture, pipe.send_text)
|
||||
except BaseException: # noqa: BLE001 - report it, then stop the whole child
|
||||
traceback.print_exc()
|
||||
os._exit(1)
|
||||
@@ -457,7 +492,19 @@ class TerminalPainter:
|
||||
self.pad = self.cw
|
||||
|
||||
def draw(self, grid): # noqa: ANN001, ANN201 - cells, an Image
|
||||
"""The image of ``grid``."""
|
||||
"""The image of ``grid``, in a palette of 256 colours.
|
||||
|
||||
A palette of 256 colours, with no dither, looks the same as the full image and
|
||||
makes the file four times smaller: a terminal has few colours, and the smooth
|
||||
edges of the text need only a few steps of each.
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
image = self.render(grid)
|
||||
return image.quantize(256, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE)
|
||||
|
||||
def render(self, grid): # noqa: ANN001, ANN201 - cells, an Image
|
||||
"""The image of ``grid``, in full RGB colour."""
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
cols, rows = len(grid[0]), len(grid)
|
||||
@@ -488,11 +535,7 @@ class TerminalPainter:
|
||||
[x, y + self.ch - self.ss * 2, x + self.cw - 1, y + self.ch - self.ss],
|
||||
fill=fg,
|
||||
)
|
||||
# A palette of 256 colours, with no dither, looks the same as the full image and
|
||||
# makes the file four times smaller: a terminal has few colours, and the smooth
|
||||
# edges of the text need only a few steps of each.
|
||||
image = image.resize((width // 2, height // 2), Image.Resampling.LANCZOS)
|
||||
return image.quantize(256, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE)
|
||||
return image.resize((width // 2, height // 2), Image.Resampling.LANCZOS)
|
||||
|
||||
@staticmethod
|
||||
def _colours(style) -> tuple[tuple[int, int, int], tuple[int, int, int]]: # noqa: ANN001
|
||||
|
||||
Reference in new issue
Block a user