rename to chirami
チラ見, a quick peek: the glance card is the whole idea.
11234566789/jpshow/chirami/dist/*.bin/testdata/out//.tmp//jpshow-ocr/chirami-ocr/result.browser-artifacts/card.png1123458 unmodified lines141516171818192021223 unmodified lines262728293030313233347 unmodified lines424344454646474849505151# jpshow design# chirami design
Select Japanese text anywhere; a small card pops up above the pointer withfurigana, romaji and a speaker, and on click the word's parts, dictionaryentries with pitch accent, and its kanji, each kanji opening a page of its8 unmodified lines## Packages
| path | what ||---|---|| `cmd/jpshow` | CLI: `run`, `setup`, `show`, `render`, `lookup`, `ocr` || `cmd/chirami` | CLI: `run`, `setup`, `show`, `render`, `lookup`, `ocr` || `internal/app` | debounce, page stack (word view, kanji pages), hover and tips, placement, hide policy || `internal/card` | pure-Go renderer: model -> RGBA image + clickable regions with tips || `internal/analyze` | text -> words with furigana and romaji, dictionary lookups, inflection parts || `internal/grammar` | Yomitan's Japanese deinflector, ported with its test suite |3 unmodified lines| `internal/jisho`, `internal/speech`, `internal/paths` | online fallback, TTS, data dirs || `internal/platform` | the Backend contract; `darwin` (cgo AppKit + AX, screenshot shortcut watch), `wayland` (hand-written wire client, layer-shell, data-control, niri screenshot events) || `internal/ocr` | client for the OCR helper: length-prefixed PNG in, one JSON line out, started on demand, stopped when idle || `macos/ocr` | the macOS helper: Live Text (VisionKit), Swift-only || `cmd/jpshow-ocr`, `internal/ocr/onnx`, `internal/ocr/fetch` | the Linux helper: meikiocr's detector and recognizer plus manga-ocr for vertical text, through onnxruntime, models pinned by hash and memory-mapped in ORT format || `cmd/chirami-ocr`, `internal/ocr/onnx`, `internal/ocr/fetch` | the Linux helper: meikiocr's detector and recognizer plus manga-ocr for vertical text, through onnxruntime, models pinned by hash and memory-mapped in ORT format |
## Card rules
- The collapsed card answers only "how do I read it": selection, furigana,7 unmodified lines grows.
## Data files
`jpshow setup` writes `morph.bin` (exported from kagome-dict/ipa, compiled`chirami setup` writes `morph.bin` (exported from kagome-dict/ipa, compiledinto the binary but only touched by setup) and `dict.bin` (downloadedsources, built in a temp dir and renamed into place) under`paths.DataDir()`. Both are little-endian with a magic and a version, readthrough mmap with no up-front parsing; a version mismatch asks for`jpshow setup` again.`chirami setup` again.112334567815 unmodified lines24252627272829303031323334353536373839401 unmodified line42434445454647484950515152535253545556565757585959606160616263646566666768696970717071727373747576747576777879808182838384848586878788899088899091929394959596969798991001011011021031041051066 unmodified lines1131141151161161171181191201218 unmodified lines`jpshow` pops up a little card at your pointer whenever you select japanese`chirami` pops up a little card at your pointer whenever you select japanesetext, with furigana, romaji, pitch accent, dictionary entries and kanji. itworks offline, on macos and linux (wayland).works offline, on macos and linux (wayland). chirami (チラ見) is a quickpeek out of the corner of your eye, which is all it asks of you.

at first the card only tells you how to read what you selected: furigana,15 unmodified lines
take a screenshot of some japanese with your usual shortcut and the card popsup for the text in it. copying an image never does that, only screenshots.
on macos jpshow watches for your screenshot shortcuts (remapped ones too) andon macos chirami watches for your screenshot shortcuts (remapped ones too) andreads the image with live text. on niri it follows niri's screenshot event andreads the image with meikiocr and manga-ocr on the cpu, after`jpshow setup -ocr` downloads the models (~500 MB). other wayland compositors`chirami setup -ocr` downloads the models (~500 MB). other wayland compositorsdon't say when you took a screenshot, so there's no ocr there yet.
## requirements
- macos 13 or newer, 14 for vertical text in screenshots. jpshow asks for- macos 13 or newer, 14 for vertical text in screenshots. chirami asks for accessibility to read the selection, and for input monitoring to see the screenshot shortcut.- linux: a wayland compositor with layer-shell and data-control (niri, sway, hyprland, river, kde, ...). gnome and x11 aren't supported.1 unmodified line
## installation
there is a nix flake, so just do`nix profile add git+https://tangled.org/ptr.pet/jpshow`, or`nix profile add git+https://tangled.org/ptr.pet/chirami`, or`nix-env -f . -i` from a checkout. it comes with the ocr helper.
otherwise get go and compile it:
```go build ./cmd/jpshow # CGO_ENABLED=0 is fine on linuxswiftc -parse-as-library -O -o jpshow-ocr macos/ocr/main.swift # macos, for screenshotsgo build ./cmd/jpshow-ocr # linux, for screenshotsgo build ./cmd/chirami # CGO_ENABLED=0 is fine on linuxswiftc -parse-as-library -O -o chirami-ocr macos/ocr/main.swift # macos, for screenshotsgo build ./cmd/chirami-ocr # linux, for screenshots```
keep `jpshow-ocr` next to `jpshow`. the linux one needs onnxruntime, point`JPSHOW_ORT_LIB` at `libonnxruntime.so` if it isn't somewhere usual.keep `chirami-ocr` next to `chirami`. the linux one needs onnxruntime, point`CHIRAMI_ORT_LIB` at `libonnxruntime.so` if it isn't somewhere usual.
then run `jpshow setup` once. it builds the tokenizer data and downloads thedictionaries (~90 MB) into `~/Library/Application Support/jpshow` or`$XDG_DATA_HOME/jpshow` (`JPSHOW_DATA` overrides), and then `jpshow` runs it.then run `chirami setup` once. it builds the tokenizer data and downloads thedictionaries (~90 MB) into `~/Library/Application Support/chirami` or`$XDG_DATA_HOME/chirami` (`CHIRAMI_DATA` overrides), and then `chirami` runs it.
## running it all the time
with home-manager, import the flake's `homeManagerModules.default` and set`services.jpshow.enable = true;` (and `services.jpshow.extraArgs` for flags).`services.chirami.enable = true;` (and `services.chirami.extraArgs` for flags).that's a systemd user service on linux and a launchd agent on macos.
otherwise on linux copy `contrib/jpshow.service` into `~/.config/systemd/user`and `systemctl --user enable --now jpshow`. it runs the jpshow on your PATH, soafter an update `systemctl --user restart jpshow` is enough.otherwise on linux copy `contrib/chirami.service` into `~/.config/systemd/user`and `systemctl --user enable --now chirami`. it runs the chirami on your PATH, soafter an update `systemctl --user restart chirami` is enough.
on macos copy `contrib/dev.dawn.jpshow.plist` into `~/Library/LaunchAgents`,point it at your jpshow (with nix that's `/Users/you/.nix-profile/bin/jpshow`)and `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.dawn.jpshow.plist`.jpshow then asks for accessibility and input monitoring for itself instead ofon macos copy `contrib/dev.dawn.chirami.plist` into `~/Library/LaunchAgents`,point it at your chirami (with nix that's `/Users/you/.nix-profile/bin/chirami`)and `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.dawn.chirami.plist`.chirami then asks for accessibility and input monitoring for itself instead ofyour terminal, and again after every update, since macos ties them to theexact binary.
## usage
```jpshow [flags] watch the selectionjpshow setup [-names] [-ocr]chirami [flags] watch the selectionchirami setup [-names] [-ocr] build the data; -names adds JMnedict (+70 MB), -ocr the linux ocr modelsjpshow show TEXT pop up the card for TEXT at the pointerjpshow render TEXT draw the card into a png (-o, -expanded, -scale)jpshow lookup TEXT print the analysis and dictionary entriesjpshow ocr FILE print the japanese read from an imagechirami show TEXT pop up the card for TEXT at the pointerchirami render TEXT draw the card into a png (-o, -expanded, -scale)chirami lookup TEXT print the analysis and dictionary entrieschirami ocr FILE print the japanese read from an image```
`-romaji macron|wapuro|plain|none` picks the romaji (tōkyō, toukyou, tokyo),`-theme night|day|auto` the colours, `-zoom` how much bigger the card is drawnon 1x displays (1.25 by default). `JPSHOW_FONT=/path/font.ttc[:index]` swapsthe font, and `JPSHOW_DEBUG=1` logs what jpshow sees (including youron 1x displays (1.25 by default). `CHIRAMI_FONT=/path/font.ttc[:index]` swapsthe font, and `CHIRAMI_DEBUG=1` logs what chirami sees (including yourselections).
## acknowledgements
jpshow ports a lot of other people's work and tests against it:chirami ports a lot of other people's work and tests against it:
- the tokenizer is a port of [kagome]'s lattice (MIT) over IPADIC- [JMdict], [KANJIDIC2], [JMnedict] and KRADFILE/RADKFILE (EDRDG, CC BY-SA), via [jmdict-simplified]6 unmodified lines- fonts on linux: M PLUS Rounded 1c and Source Sans 3 (SIL OFL 1.1)
llms were also used while making this (a lot of it, honestly ^^).
jpshow is GPL-3.0-or-later.chirami is GPL-3.0-or-later.
[kagome]: https://github.com/ikawaha/kagome[JMdict]: https://www.edrdg.org/jmdict/j_jmdict.html[KANJIDIC2]: https://www.edrdg.org/wiki/index.php/KANJIDIC_Project8 unmodified lines12 unmodified lines13141516171817181920212212 unmodified lines353637383940394041424344454445464748495049505152535455545556575859606161626364656667676869707172727374757677787879808182838485868687888912 unmodified lines let rel = lib.removePrefix (toString ./. + "/") (toString path); in !(lib.elem rel [ "jpshow" "jpshow-ocr" "chirami" "chirami-ocr" "card.png" "dist" "result" ".tmp"12 unmodified lines # the darwin backend is cgo; the linux one is pure go main = pkgs.buildGoModule ( common // { pname = "jpshow"; subPackages = [ "cmd/jpshow" ]; pname = "chirami"; subPackages = [ "cmd/chirami" ]; env.CGO_ENABLED = if stdenv.hostPlatform.isDarwin then "1" else "0"; } ); # the screenshot ocr helper, started by jpshow on demand. on linux it runs # onnx models through onnxruntime (cgo), kept out of jpshow itself # the screenshot ocr helper, started by chirami on demand. on linux it runs # onnx models through onnxruntime (cgo), kept out of chirami itself ocrLinux = pkgs.buildGoModule ( common // { pname = "jpshow-ocr"; subPackages = [ "cmd/jpshow-ocr" ]; pname = "chirami-ocr"; subPackages = [ "cmd/chirami-ocr" ]; env.CGO_ENABLED = "1"; nativeBuildInputs = [ pkgs.makeWrapper ]; postInstall = '' wrapProgram $out/bin/jpshow-ocr \ --set-default JPSHOW_ORT_LIB ${pkgs.onnxruntime}/lib/libonnxruntime.so wrapProgram $out/bin/chirami-ocr \ --set-default CHIRAMI_ORT_LIB ${pkgs.onnxruntime}/lib/libonnxruntime.so ''; } ); # on macos it is live text, which is swift-only ocrDarwin = stdenv.mkDerivation { pname = "jpshow-ocr"; pname = "chirami-ocr"; inherit version; src = ./macos/ocr; nativeBuildInputs = [ pkgs.swift ]; buildPhase = '' runHook preBuild swiftc -parse-as-library -O -o jpshow-ocr main.swift swiftc -parse-as-library -O -o chirami-ocr main.swift runHook postBuild ''; installPhase = '' runHook preInstall install -Dm755 jpshow-ocr $out/bin/jpshow-ocr install -Dm755 chirami-ocr $out/bin/chirami-ocr runHook postInstall ''; };inpkgs.symlinkJoin { name = "jpshow-${version}"; name = "chirami-${version}"; paths = [ main (if stdenv.hostPlatform.isDarwin then ocrDarwin else ocrLinux) ]; meta = { description = "select japanese text, get furigana, romaji and dictionary entries at the pointer"; license = lib.licenses.gpl3Plus; mainProgram = "jpshow"; mainProgram = "chirami"; platforms = lib.platforms.linux ++ lib.platforms.darwin; };}122345628 unmodified lines{ description = "jpshow"; description = "chirami";
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; parts.url = "github:hercules-ci/flake-parts";28 unmodified lines11234511 unmodified linesmodule jpshowmodule chirami
go 1.26.0
require (11 unmodified lines1234567891011121314# a systemd user unit: copy it into ~/.config/systemd/user, then# systemctl --user enable --now chirami[Unit]Description=chirami: furigana and dictionary cards for selected japanesePartOf=graphical-session.targetAfter=graphical-session.target
[Service]# chirami from the user's PATH, so updates only need a restartExecStart=/usr/bin/env chiramiRestart=on-failure
[Install]WantedBy=graphical-session.target1234567891011121314151617181920<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>dev.dawn.chirami</string> <key>ProgramArguments</key> <array> <string>/usr/local/bin/chirami</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>ProcessType</key> <string>Interactive</string> <key>StandardErrorPath</key> <string>/tmp/chirami.log</string></dict></plist>1234567891011121314151617181920<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>dev.dawn.jpshow</string> <key>ProgramArguments</key> <array> <string>/usr/local/bin/jpshow</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>ProcessType</key> <string>Interactive</string> <key>StandardErrorPath</key> <string>/tmp/jpshow.log</string></dict></plist>112334567891011111213141516151617181920212021222324251 unmodified line272829303131323334353637383939404141424344453 unmodified lines495051525353545556578 unmodified lines# home-manager module: services.jpshow.enable = true runs jpshow as a# home-manager module: services.chirami.enable = true runs chirami as a# systemd user service with your graphical session on linux, or as a launchd# agent on macos. `jpshow setup` still has to run once.# agent on macos. `chirami setup` still has to run once.{ config, lib, pkgs, ...}:let cfg = config.services.jpshow; cfg = config.services.chirami; command = [ (lib.getExe cfg.package) ] ++ cfg.extraArgs;in{ options.services.jpshow = { enable = lib.mkEnableOption "jpshow, furigana and dictionary cards for selected japanese"; options.services.chirami = { enable = lib.mkEnableOption "chirami, furigana and dictionary cards for selected japanese"; package = lib.mkOption { type = lib.types.package; default = import ../default.nix { inherit pkgs; }; defaultText = lib.literalExpression "jpshow from this repository"; description = "the jpshow package to run"; defaultText = lib.literalExpression "chirami from this repository"; description = "the chirami package to run"; }; extraArgs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ];1 unmodified line "-offline" "-romaji" "wapuro" ]; description = "flags passed to jpshow"; description = "flags passed to chirami"; }; };
config = lib.mkIf cfg.enable ( lib.mkMerge [ { home.packages = [ cfg.package ]; } (lib.mkIf pkgs.stdenv.hostPlatform.isLinux { systemd.user.services.jpshow = { systemd.user.services.chirami = { Unit = { Description = "jpshow: furigana and dictionary cards for selected japanese"; Description = "chirami: furigana and dictionary cards for selected japanese"; PartOf = [ "graphical-session.target" ]; After = [ "graphical-session.target" ]; }; Service = {3 unmodified lines Install.WantedBy = [ "graphical-session.target" ]; }; }) (lib.mkIf pkgs.stdenv.hostPlatform.isDarwin { launchd.agents.jpshow = { launchd.agents.chirami = { enable = true; config = { ProgramArguments = command; RunAtLoad = true;8 unmodified lines1234567891011121314# a systemd user unit: copy it into ~/.config/systemd/user, then# systemctl --user enable --now jpshow[Unit]Description=jpshow: furigana and dictionary cards for selected japanesePartOf=graphical-session.targetAfter=graphical-session.target
[Service]# jpshow from the user's PATH, so updates only need a restartExecStart=/usr/bin/env jpshowRestart=on-failure
[Install]WantedBy=graphical-session.target123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102package main
import ( "bytes" "context" "errors" "flag" "fmt" "image" _ "image/jpeg" _ "image/png" "io" "log" "os" "path/filepath"
"chirami/internal/ocr" "chirami/internal/ocr/fetch" "chirami/internal/ocr/onnx" "chirami/internal/paths")
// defaultModelDir is where `chirami setup -ocr` puts the models.func defaultModelDir() string { if env := os.Getenv("CHIRAMI_OCR_MODELS"); env != "" { return env } return filepath.Join(paths.DataDir(), "ocr")}
func main() { log.SetFlags(0) log.SetPrefix("chirami-ocr: ")
modelsFlag := flag.String("models", defaultModelDir(), "path to directory containing OCR models") ortLibFlag := flag.String("ort-lib", "", "explicit path to libonnxruntime shared library") downloadFlag := flag.Bool("download", false, "ensure all models are downloaded to -models dir and exit") prepareFlag := flag.Bool("prepare", false, "load every model once (writing the ORT-format copies) and exit") flag.Parse()
modelsDir := *modelsFlag
if *downloadFlag { log.Printf("checking and downloading models into %s...", modelsDir) if err := fetch.EnsureModels(context.Background(), modelsDir); err != nil { log.Fatalf("download models: %v", err) } log.Printf("all models verified in %s", modelsDir) return }
// setup verified the hashes; here only a cheap check that nothing is // missing, since hashing 480 MB would slow every start if missing := fetch.Absent(modelsDir); len(missing) > 0 { log.Fatalf("%d model file(s) missing in %s; run `chirami setup -ocr`", len(missing), modelsDir) }
if err := onnx.InitRuntime(*ortLibFlag); err != nil { log.Fatalf("init onnxruntime: %v", err) }
pipe, err := onnx.NewPipeline(modelsDir) if err != nil { log.Fatalf("initialize pipeline: %v", err) } defer pipe.Close() if *prepareFlag { return }
// Main request loop: read 4-byte big-endian length + PNG on stdin, reply JSON on stdout for { req, err := ocr.ReadRequest(os.Stdin) if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return } log.Fatalf("read request: %v", err) }
img, _, err := image.Decode(bytes.NewReader(req)) if err != nil { _ = ocr.WriteResult(os.Stdout, ocr.Result{ Engine: "onnx", Error: fmt.Sprintf("decode image: %v", err), }) continue }
res, err := pipe.Recognize(img) if err != nil { res = ocr.Result{ Engine: "onnx", Error: err.Error(), } }
if err := ocr.WriteResult(os.Stdout, res); err != nil { log.Fatalf("write result: %v", err) } }}12345678package main
import ( "chirami/internal/platform" "chirami/internal/platform/darwin")
func runBackend(app func(platform.Backend) error) error { return darwin.Run(app) }12345678910package main
import ( "chirami/internal/platform" "chirami/internal/platform/wayland")
func runBackend(app func(platform.Backend) error) error { return wayland.Run(app)}