diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 864f42b..104bd18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,18 @@ jobs: fi echo "tag=$tag" >> "$GITHUB_OUTPUT" + # PyPI keeps each release's description for good, so point its images + # at this release's tag rather than at main, which moves on + - name: Pin README images to the release tag + env: + TAG: ${{ steps.tag.outputs.tag }} + run: | + sed -i "s#raw.githubusercontent.com/$GITHUB_REPOSITORY/main/#raw.githubusercontent.com/$GITHUB_REPOSITORY/$TAG/#g" README.md + if grep -q "raw.githubusercontent.com/$GITHUB_REPOSITORY/main/" README.md; then + echo "::error::README.md still links to main" + exit 1 + fi + - name: Build and check the distributions run: | python -m pip install build twine diff --git a/README.md b/README.md index 65bc1c9..cf8826a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@

- Vayu - The Fastest Whisper on Apple Silicon + + + Vayu (وایو) +

# Vayu - The Fastest Whisper on Apple Silicon diff --git a/assets/banner.png b/assets/banner.png deleted file mode 100644 index a64eaa3..0000000 Binary files a/assets/banner.png and /dev/null differ diff --git a/assets/logo-dark.svg b/assets/logo-dark.svg new file mode 100644 index 0000000..2b6c5a3 --- /dev/null +++ b/assets/logo-dark.svg @@ -0,0 +1,10 @@ + +Vayu (وایو) + + + + + + + + diff --git a/assets/logo-light.svg b/assets/logo-light.svg new file mode 100644 index 0000000..baeae8a --- /dev/null +++ b/assets/logo-light.svg @@ -0,0 +1,10 @@ + +Vayu (وایو) + + + + + + + + diff --git a/scripts/make_logo.py b/scripts/make_logo.py new file mode 100644 index 0000000..21a2dc4 --- /dev/null +++ b/scripts/make_logo.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Build the README logo: assets/logo-light.svg and assets/logo-dark.svg. + +The mark is three wind strokes; the text ("VAYU" in Manrope ExtraBold and +"وایو" in Vazirmatn SemiBold) is shaped with HarfBuzz and written as outlines, +so the SVGs render the same everywhere without web fonts. + +Usage: + pip install fonttools uharfbuzz + python scripts/make_logo.py + +Fonts are downloaded from Google Fonts (both are under the SIL Open Font +License) into ~/.cache/vayu-logo-fonts, one file per font version, so a new +upstream version is fetched rather than mixed with an old cached copy. +""" + +import math +import os +import pathlib +import re +import urllib.request + +import uharfbuzz as hb +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.svgLib.path import parse_path +from fontTools.ttLib import TTFont + +ASSETS = pathlib.Path(__file__).resolve().parent.parent / "assets" +CACHE = ( + pathlib.Path(os.environ.get("XDG_CACHE_HOME") or pathlib.Path.home() / ".cache") + / "vayu-logo-fonts" +) +TIMEOUT = 30 # seconds per request + +PALETTES = { + # for light backgrounds + "light": dict(ink="#0F172A", a1="#0F766E", a2="#EA580C", muted="#57606A"), + # for dark backgrounds + "dark": dict(ink="#F1F5F9", a1="#2DD4BF", a2="#FB923C", muted="#8B949E"), +} + +# the mark: three wind strokes on an 80-unit grid, as (palette colour, path) +MARK_STROKES = [ + ("a1", "M8 30 H52 A10 10 0 1 0 42 20"), + ("ink", "M8 48 H58 A8 8 0 1 1 50 56"), + ("a2", "M18 66 H36"), +] +MARK_SCALE, MARK_STROKE = 1.1, 7 # grid units -> px, stroke width in grid units +GAP = 20 # px between the mark's ink and the text's ink +MARGIN = 2 # px of space around all the ink +WORD_SIZE, WORD_TRACKING = 54, 0.02 # px, em +FA_SIZE, FA_GAP = 22, 10 + + +def num(v): + return f"{v:.2f}".rstrip("0").rstrip(".") + + +def union(*boxes): + return ( + min(b[0] for b in boxes), + min(b[1] for b in boxes), + max(b[2] for b in boxes), + max(b[3] for b in boxes), + ) + + +def mark_ink(): + """Ink box of the mark in px: the strokes' centre lines plus half a stroke.""" + pen = BoundsPen(None) + for _, d in MARK_STROKES: + parse_path(d, pen) + half = MARK_STROKE / 2 + x0, y0, x1, y1 = pen.bounds + return tuple(v * MARK_SCALE for v in (x0 - half, y0 - half, x1 + half, y1 + half)) + + +def google_font(family: str, weight: int) -> pathlib.Path: + """Download a static TTF of `family` at `weight`, cached per font version.""" + css_url = ( + "https://fonts.googleapis.com/css2?family=" + f"{family.replace(' ', '+')}:wght@{weight}" + ) + css = urllib.request.urlopen(css_url, timeout=TIMEOUT).read().decode() + ttf_urls = re.findall(r"url\((https://fonts\.gstatic\.com/[^)]+\.ttf)\)", css) + if len(ttf_urls) != 1: + raise RuntimeError( + f"Expected one TTF for {family} {weight} from Google Fonts, got " + f"{len(ttf_urls)}; the CSS API may have changed:\n{css}" + ) + # the URL path carries the font version (e.g. /s/manrope/v20/...) + path = CACHE / re.sub(r"[^A-Za-z0-9.]+", "-", ttf_urls[0].split("/s/", 1)[1]) + if not path.exists(): + CACHE.mkdir(parents=True, exist_ok=True) + data = urllib.request.urlopen(ttf_urls[0], timeout=TIMEOUT).read() + partial = path.with_suffix(".part") + partial.write_bytes(data) + os.replace(partial, path) # never leave a truncated font in the cache + return path + + +class Text: + """A shaped line of text, positioned in SVG pixels.""" + + def __init__(self, path, text, size, tracking=0.0, lang=None): + self.font = TTFont(path) + missing = [c for c in text if ord(c) not in self.font.getBestCmap()] + if missing: + raise RuntimeError(f"{path.name} has no glyphs for {missing!r}") + hbfont = hb.Font(hb.Face(hb.Blob.from_file_path(str(path)))) + buf = hb.Buffer() + buf.add_str(text) + buf.guess_segment_properties() # direction and script, e.g. RTL Arabic + if lang: + buf.language = lang + hb.shape(hbfont, buf) + + self.scale = size / self.font["head"].unitsPerEm + order = self.font.getGlyphOrder() + self.glyphs = [] + x = 0.0 + # glyphs come back in visual (left-to-right) order, RTL included + for info, pos in zip(buf.glyph_infos, buf.glyph_positions): + self.glyphs.append( + ( + order[info.codepoint], + x + pos.x_offset * self.scale, + pos.y_offset * self.scale, + ) + ) + x += pos.x_advance * self.scale + tracking * size + self.width = x - tracking * size # no tracking after the last glyph + hhea = self.font["hhea"] + self.ascent = hhea.ascent * self.scale + self.descent = -hhea.descent * self.scale + + def _draw(self, pen, left, baseline): + glyphset = self.font.getGlyphSet() + s = self.scale + for name, gx, gy in self.glyphs: + # font units (y up) -> SVG pixels (y down) + glyphset[name].draw( + TransformPen(pen, (s, 0, 0, -s, left + gx, baseline - gy)) + ) + + def path(self, left, baseline): + pen = SVGPathPen(self.font.getGlyphSet(), ntos=num) + self._draw(pen, left, baseline) + return pen.getCommands() + + def bounds(self, left, baseline): + pen = BoundsPen(self.font.getGlyphSet()) + self._draw(pen, left, baseline) + return pen.bounds + + def baseline_in_box(self, top, line_height): + """Baseline of this text centred in a line box, as CSS lays it out.""" + half_leading = (line_height - (self.ascent + self.descent)) / 2 + return top + half_leading + self.ascent + + +def main(): + word = Text(google_font("Manrope", 800), "VAYU", WORD_SIZE, WORD_TRACKING) + persian = Text(google_font("Vazirmatn", 600), "وایو", FA_SIZE, lang="fa") + + # set the two lines at the origin, as CSS line boxes would stack them + word_baseline = word.baseline_in_box(0, WORD_SIZE) + fa_baseline = persian.baseline_in_box(WORD_SIZE + FA_GAP, FA_SIZE) + word_ink = word.bounds(0, word_baseline) + # right-align the visible letters (not the advance boxes) under VAYU + fa_left = word_ink[2] - persian.bounds(0, fa_baseline)[2] + text_ink = union(word_ink, persian.bounds(fa_left, fa_baseline)) + + # put the text GAP px right of the mark's ink, centred on it vertically + mark = mark_ink() + dx = mark[2] + GAP - text_ink[0] + dy = (mark[1] + mark[3] - text_ink[1] - text_ink[3]) / 2 + ink = union( + mark, (text_ink[0] + dx, text_ink[1] + dy, text_ink[2] + dx, text_ink[3] + dy) + ) + + # then shift everything so the frame holds all the ink with MARGIN to spare + ox, oy = MARGIN - ink[0], MARGIN - ink[1] + width = math.ceil(ink[2] - ink[0] + 2 * MARGIN) + height = math.ceil(ink[3] - ink[1] + 2 * MARGIN) + word_d = word.path(dx + ox, word_baseline + dy + oy) + fa_d = persian.path(fa_left + dx + ox, fa_baseline + dy + oy) + + for mode, c in PALETTES.items(): + strokes = "\n".join( + f'' for colour, d in MARK_STROKES + ) + svg = f""" +Vayu (وایو) + +{strokes} + + + + +""" + out = ASSETS / f"logo-{mode}.svg" + out.write_text(svg, encoding="utf-8") + print(f"wrote {out} ({width}x{height})") + + +if __name__ == "__main__": + main()