Recently Written · git

subread-anki

One tap makes an Anki card on Android: word, definition, sentence, screenshot, audio. Works with any app, with SubRead Dictionary and SubRead Overlay.

git clone https://github.com/equwal/subread-anki

Log | Files | Refs


README.md (12730 bytes)

1 # SubRead Anki
2 
3 A pop-up for Anki cards, used the same way as a dictionary. Select a text
4 in any app (a reader, a browser, an OCR app) and choose "SubRead Anki" in the
5 text selection menu. The pop-up opens over the app and shows the text and
6 the terms at the word, from
7 [SubRead Dictionary](https://github.com/equwal/subread-dictionary). A tap on
8 a character of the text moves the word there. Tap "+ Anki" on a term: the
9 card goes into [AnkiDroid](https://github.com/ankidroid/Anki-Android) with
10 the word, its reading and definition, the sentence around the word, a
11 picture of the screen and the sound of the sentence.
12 
13 The app is on its own: it needs no other SubRead app. With SubRead
14 Dictionary, the pop-up has the definitions. With
15 [SubRead Overlay](https://github.com/equwal/subread-overlay), a subtitle
16 word shows its whole line, and the card gets the sound of that line.
17 
18 Nothing leaves the device. The app talks to AnkiDroid and SubRead
19 Dictionary on the device, and opens a URL only when a sender gives one for
20 an audio or a picture.
21 
22 ## How to use it
23 
24 1. Install AnkiDroid and allow SubRead Anki to add cards to it. AnkiDroid
25    must have "Enable AnkiDroid API" on, under Advanced in its settings; it
26    is on by default.
27 2. Choose the deck. Choose the note type, or let the app make its own,
28    "SubRead Anki". The app guesses what goes into each field from the field
29    names: the [Lapis](https://github.com/donkuri/lapis) note type and the
30    basic ones work as they are. "Fields" changes the guess.
31 3. Install SubRead Dictionary and import a dictionary into it, for the
32    reading, the definition and the word audio.
33 4. For a picture and sound on the cards, start the capture, from the app or
34    from its tile in the quick settings. Android asks each time. While the
35    capture is on, the app keeps the last minute and a half of the sound of
36    the device and the newest picture of the screen. Stop it from the
37    notification or the tile.
38 
39 Then, in any app: select a sentence, choose "SubRead Anki", tap the word in the
40 pop-up, and tap "+ Anki". A single selected word works too; the card then
41 has no sentence, unless the word is in the subtitle line of SubRead Overlay.
42 The button says "✓ Added", and a term that is in the deck already says
43 "✓ In Anki". A tap outside the pop-up closes it.
44 
45 Some browsers (Firefox, Brave) show only some of the apps in the text
46 selection menu. There, choose Share and then "SubRead Anki". The app
47 removes the link of the page and the quotes that the browser adds.
48 
49 The line under the text says what the card gets: "Picture ✓   Sound 5.3 s ✓
50   Deck: SubRead". The picture is the screen at the moment the text was
51 selected, from before the pop-up opened. The sound is the last eight
52 seconds before that moment: the audiobook that plays while you read. A clip
53 of silence is left out.
54 
55 Share a picture to "SubRead Anki" within an hour of a card, and it goes onto
56 that card. The option "add the first term at once, with no pop-up" makes a
57 selection a card with no pop-up.
58 
59 ## What goes on the card
60 
61 | Part | Where it comes from |
62 |---|---|
63 | Expression, reading, definition | The term that you tap "+ Anki" on, from SubRead Dictionary. `食べた` gives the card `食べる`. A sender can give its own. |
64 | Sentence | The sentence around the word in the text of the pop-up. The word is in `<b>` as it is in the sentence. |
65 | Sentence audio | The sender's file, cut to the times it gives. Else the capture: the subtitle line of SubRead Overlay, cut on the clock of the player with a pad on each side, or the eight seconds before the text came in. AAC in `.m4a`: every player and every Anki has it. |
66 | Word audio | The sender, else the audio of SubRead Dictionary (the local audio server, or a remote source when on). |
67 | Picture | The sender, else the screen from the capture at the moment the text came in. JPEG, at most 1600 pixels on the long side. |
68 | Source, tags | The sender, else the name of the app that sent the text. The tags of the settings go on every card. |
69 
70 The note type "SubRead Anki" has the fields Expression, Reading, Definition,
71 Sentence, SentenceAudio, WordAudio, Image and Source, and one card: the word
72 and the sentence in front, the rest behind, black on white.
73 
74 ## For other apps
75 
76 Four doors, all into one activity, `space.subread.anki.AddActivity`. Any
77 part can be left out: the app fills it in as the table above says. A
78 request with a `definition` is a whole card: it goes to AnkiDroid at once,
79 with no pop-up, and a toast says so. A request without one opens the
80 pop-up at the word, so the user picks the term. `confirm` changes that.
81 
82 **The text selection menu and the share sheet.** No code: the app has an
83 entry in both. `Intent.ACTION_PROCESS_TEXT` with `EXTRA_PROCESS_TEXT`, or
84 `Intent.ACTION_SEND` with `text/plain` and `EXTRA_TEXT`. The text is the
85 text of the pop-up. `ACTION_SEND` with an `image/*` stream and a text is the
86 picture and the text; the picture alone goes onto the last card.
87 
88 **An Intent from an app on the device.** The action
89 `space.subread.anki.action.ADD`, with the keys of the table below as extras,
90 each with the prefix `space.subread.anki.extra.` and in upper case:
91 
92 ```kotlin
93 val add = Intent("space.subread.anki.action.ADD").apply {
94     setPackage("space.subread.anki")
95     putExtra("space.subread.anki.extra.EXPRESSION", "食べた")
96     putExtra("space.subread.anki.extra.SENTENCE", "猫が魚を食べた。")
97     putExtra("space.subread.anki.extra.AUDIO", audioUri)        // Uri, or a String
98     putExtra("space.subread.anki.extra.AUDIO_START", 96_000L)  // Long, or a String
99     putExtra("space.subread.anki.extra.AUDIO_END", 99_500L)
100     putExtra("space.subread.anki.extra.IMAGE", pageUri)
101     putExtra("space.subread.anki.extra.SOURCE", "吾輩は猫である")
102     clipData = ClipData.newRawUri("audio", audioUri).apply { addItem(ClipData.Item(pageUri)) }
103     addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
104 }
105 startActivityForResult(add, REQUEST_CARD)   // or startActivity
106 ```
107 
108 A `content://` Uri must be one the app may grant: its own `FileProvider`, or
109 a document Uri it holds a permission for. A `file://` Uri is refused. The
110 extras of the intent API of the `main` branch are taken too, with their
111 meaning there: `WORD`, `TEXT`, `AUDIO` (the word audio), `SENTENCE_AUDIO`,
112 `SHOW`. The "Anki" buttons of SubRead Overlay and SubRead Dictionary send
113 those. Declare the package in the manifest of the calling app, or Android 11
114 and later hide it:
115 
116 ```xml
117 <queries>
118     <package android:name="space.subread.anki" />
119 </queries>
120 ```
121 
122 The result, for a caller that started the activity for one: `RESULT_OK` with
123 `space.subread.anki.extra.NOTE_ID` (`Long`, the note in AnkiDroid), or
124 `RESULT_CANCELED` with `space.subread.anki.extra.ERROR` (`String`): `empty`,
125 `no_ankidroid`, `no_permission`, `duplicate`, `cancelled`, or what AnkiDroid
126 said.
127 
128 **A link on a web page.** The same keys, percent-encoded, in the query of
129 `subreadanki://add`. A web reader that has the sentence, the audiobook and
130 the times makes the card with one link:
131 
132 ```javascript
133 const query = new URLSearchParams({
134   expression: "食べた",
135   sentence: "猫が魚を食べた。",
136   audio: "https://example.com/books/neko.m4b",   // the whole file: the app reads the range alone
137   audio_start: "96000",
138   audio_end: "99500",
139   image: "https://example.com/books/neko/page-12.jpg",
140   source: "吾輩は猫である",
141 }).toString();
142 // Chrome: an intent link, with a fallback for a phone without the app.
143 location.href = "intent://add?" + query + "#Intent;scheme=subreadanki;package=space.subread.anki;" +
144   "S.browser_fallback_url=" + encodeURIComponent("https://github.com/equwal/subread-anki") + ";end";
145 // Firefox and the others: the plain link.
146 // location.href = "subreadanki://add?" + query;
147 ```
148 
149 The link must come from a tap of the user: browsers block an app link
150 without one. An `http(s)` audio is read with range requests, so a whole
151 audiobook is fine; a picture is fetched whole. A `data:` Uri works for a
152 small file. With a `definition`, the app shows a toast and goes back to the
153 browser; without one, the pop-up opens over the browser.
154 
155 **The keys.** Each is optional; a card needs `expression` or `sentence`.
156 
157 | Key | Value |
158 |---|---|
159 | `expression` | The word, as it is in the text. The pop-up opens at it. |
160 | `reading` | The reading of the word. |
161 | `definition` | The definition, HTML or text. Given, the card is added at once and the dictionary is not asked. |
162 | `sentence` | The sentence, plain text: the text of the pop-up. |
163 | `audio` | A Uri of the audio of the sentence: `content://`, `https://`, `http://` or `data:`. |
164 | `audio_start`, `audio_end` | With `audio`: the clip, in milliseconds of the audio. Without them, the whole file is the clip. |
165 | `word_audio` | A Uri of the audio of the word alone. |
166 | `image` | A Uri of the picture. |
167 | `source` | Where the sentence is from: the title of the book or the video. |
168 | `tags` | Anki tags, separated by spaces. |
169 | `pitch`, `frequency` | Text for the pitch and frequency fields. |
170 | `position` | The position of the player at the tap, in milliseconds. With the capture on and no `audio`, the clip is cut around it. |
171 | `cue_start`, `cue_end` | The subtitle line, in milliseconds on the clock of the player. With the capture on and no `audio`, the clip is this range. |
172 | `confirm` | `1`: open the pop-up, also with a definition. `0`: add at once, with the first term of the dictionary when there is no definition. |
173 
174 ## How it works
175 
176 `:core` is plain Kotlin, with no Android in it, and has property tests: the
177 request and its query, the text of the pop-up and the sentence around a
178 word, the clock of a player report, the ring of captured sound and its
179 loudness, the fields of a note type. `:app` has the pop-up, the capture
180 service, the AnkiDroid client and the two content provider clients.
181 
182 The pop-up takes the picture when the text comes in, before its own window
183 draws, and cuts the sound at once too. Each "+ Anki" of the same pop-up
184 uses the same picture and sound.
185 
186 The sound of the capture is 16-bit PCM in a ring with a clock. A player
187 reports a position, the time of that report and the speed; the app maps the
188 times of the subtitle line onto the clock of the ring and reads that range
189 out. A pause reported a minute ago still maps its media times to the right
190 moments. The clip is encoded with the AAC encoder of Android.
191 
192 SubRead Overlay answers `content://space.subread.overlay.player/line` with
193 the line of now and the report of the player. SubRead Dictionary answers
194 `content://space.subread.dictionary.lookup/terms?text=…` with the terms,
195 and gives the word audio through the same provider. Their READMEs have the
196 details.
197 
198 ## Build
199 
200 ```
201 ./gradlew :core:test :app:assembleDebug
202 ```
203 
204 `-PplayStore=true` leaves the Ko-fi link out of the build for Google Play.
205 
206 ## More projects
207 
208 - [SubRead](https://subread.space/): read along with an audiobook, in the browser.
209   Also [for Android](https://github.com/equwal/subread-android/releases/latest),
210   [for YouTube](https://github.com/equwal/subread-extension/releases/latest)
211   and [for KOReader](https://github.com/equwal/subread.koplugin).
212 - [SubRead Overlay](https://github.com/equwal/subread-overlay/releases/latest): subtitle lines over any Android media player.
213 - [SubRead Dictionary](https://github.com/equwal/subread-dictionary/releases/latest): a pop-up dictionary for Android that reads Yomitan dictionaries.
214 - [Subrep](https://github.com/equwal/subrep-android/releases/latest): live captions of the sound of your phone.
215 - [Book Simulator](https://booksimulator.com/): a reading room for Aozora Bunko and Project Gutenberg books.
216 - [honjimaku.com](https://honjimaku.com/): subtitles for Japanese audiobooks.
217 - [sbm Sync](https://sbmsync.com/): your bookmarks, the same on every device,
218   with [sbm](https://github.com/equwal/sbm) for dmenu,
219   [sbm for Android](https://github.com/equwal/sbm-android/releases/latest)
220   and the [sbm add-on](https://github.com/equwal/sbm-extension/releases/latest) for Firefox and Chrome.
221 - [Rebind](https://github.com/equwal/rebind/releases): remap the hardware buttons of e-ink readers and Android,
222   with [Ink Recents](https://github.com/equwal/ink-recents/releases/latest),
223   [Ink Dim](https://github.com/equwal/ink-dim/releases/latest)
224   and [Ink Update](https://github.com/equwal/ink-update/releases/latest).
225 - [dickt.store](https://dickt.store/): language-learning tools, flashcards and web toys.
226 - [hentaibun.online](https://hentaibun.online/): learn kanbun and kobun.
227 - [Recently Written](https://recentlywritten.com/): the blog, and a list of [all projects](https://recentlywritten.com/projects.html).
228 
229 ## Licence
230 
231 AGPL-3.0. See `LICENSE`.