Skip to content

Commit 806d8cc

Browse files
author
Talha Altınel
authored
add anki runner (#15)
* undo toolchain * fmt * add CleanedText method for records * shell completions * add IsSpeed func * finish up anki features * bug fixes * shuffle the row * fmt * fmt * remove errdiff package * test CleanedText * update csv names * tests for ReadCSVRecords * ensure directory exists * tests for WriteCSVRecords * lint * anki media path tests * gofumpt and note-type.apkg docs * tests * tests * lint * coveralls is down with 504 so skipping coverage uploads * add flags structs * tidy
1 parent 6118dc2 commit 806d8cc

23 files changed

Lines changed: 1521 additions & 110 deletions

.github/workflows/tests.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Install Goveralls
3030
run: go install github.com/mattn/goveralls@v0.0.12
3131

32-
- name: Upload Coverage
33-
env:
34-
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35-
run: goveralls -ignore=cmd/**/*.go -coverprofile=covprofile -service=github
32+
# - name: Upload Coverage
33+
# env:
34+
# COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
# run: goveralls -ignore=cmd/**/*.go -coverprofile=covprofile -service=github

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.mp3
22
.idea/
3-
.code/
3+
.code/
4+
main

.golangci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ formatters:
103103
- gci
104104
- gofmt
105105
- goimports
106+
- gofumpt
106107
settings:
107108
gci:
108109
sections:

README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@
99

1010
the goddess of the thieves, helps you steal translation speeches from the G-daddy monopoly.
1111

12-
<img src="https://github.com/user-attachments/assets/d1d344c9-f36b-4cf7-af70-f162f93ea9f0" width="400" alt="squash-goose">
12+
<img src="https://github.com/user-attachments/assets/d1d344c9-f36b-4cf7-af70-f162f93ea9f0" width="400" alt="goddess-of-thieves">
13+
14+
## Installation
15+
16+
### Grab Binaries
17+
18+
You can find the binaries through GitHub [releases](https://github.com/mrwormhole/laverna/releases/).
1319

1420
### Install Via Go
1521

1622
```shell
1723
go install github.com/mrwormhole/laverna@latest
1824
```
1925

20-
### Grab Binaries
21-
22-
You can find binaries through GitHub releases.
26+
## Sample Usage
2327

24-
### Sample Usage
28+
### Basic
2529

2630
Let's create example CSV
2731

@@ -49,11 +53,47 @@ or you could do YAML
4953
Running below command will generate audios in the same directory.
5054
5155
```shell
52-
laverna -file example.csv
56+
laverna run -file example.csv
5357
```
5458

5559
or
5660

5761
```shell
58-
laverna -file example.yaml
62+
laverna run -file example.yaml
63+
```
64+
65+
### Anki
66+
67+
Make sure you have note type installed for Anki. Here is the [note type](./note-type.apkg), when you imported it, you will see "Cloze Multi Choice Audio" note type in your `Anki > Tools > Manage Note Types`. Then you can proceed with the below Anki CSV format.
68+
69+
```csv
70+
Text,HelperText,TextA,TextB,TextC,TextD
71+
ฉันชอบ{{c1::ฟัง}}เพลง,I like to listen to music,ฟัง,เล่น,ดู,อ่าน
72+
```
73+
74+
```shell
75+
laverna anki --profile Talha --voice th --file ./testdata/anki-th-example.csv
5976
```
77+
78+
Now you will see a new result as below CSV file, all of your media is generated and inserted into Anki, however in order to create a deck in Anki, you need to import CSV below. And carefully choose comma delimiter with note type "Cloze Multi Choice Audio"
79+
80+
```csv
81+
ฉันชอบ{{c1::ฟัง}}เพลง,I like to listen to music,ฟัง,เล่น,ดู,อ่าน,[sound:a.mp3],[sound:b.mp3],[sound:c.mp3],[sound:d.mp3],[sound:e.mp3]
82+
```
83+
84+
![anki-front-card](https://github.com/user-attachments/assets/fbc19bd1-0d74-4659-b9a0-2e40e9e8d4cf)
85+
86+
![anki-back-card](https://github.com/user-attachments/assets/6cca4cfc-237a-493f-8306-0972278231fa)
87+
88+
## Shell Completions
89+
90+
Output shell completion script for bash, zsh, fish, or Powershell.
91+
Source the output to enable completion.
92+
93+
### Bash / Zsh
94+
95+
```source <(laverna completion bash)``` or ```source <(laverna completion zsh)```
96+
97+
### Fish
98+
99+
```laverna completion fish > ~/.config/fish/completions/laverna.fish```

anki/record.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package anki
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"io"
7+
"math/rand/v2"
8+
"strings"
9+
)
10+
11+
// Record is how anki CSV file should look like
12+
type Record struct {
13+
Text string
14+
HelperText string
15+
TextA, TextB, TextC, TextD string
16+
AudioA, AudioB, AudioC, AudioD string
17+
AudioAnswer string
18+
}
19+
20+
// Row returns Record in correct order
21+
func (r Record) Row() []string {
22+
return []string{
23+
r.Text,
24+
r.HelperText,
25+
r.TextA,
26+
r.TextB,
27+
r.TextC,
28+
r.TextD,
29+
r.AudioA,
30+
r.AudioB,
31+
r.AudioC,
32+
r.AudioD,
33+
r.AudioAnswer,
34+
}
35+
}
36+
37+
// RandomizedRow returns Record with shuffled order of TextA, TextB, TextC, TextD
38+
func (r Record) RandomizedRow() []string {
39+
type pair struct {
40+
text string
41+
audio string
42+
}
43+
pairs := []pair{
44+
{r.TextA, r.AudioA},
45+
{r.TextB, r.AudioB},
46+
{r.TextC, r.AudioC},
47+
{r.TextD, r.AudioD},
48+
}
49+
50+
rand.Shuffle(len(pairs), func(i, j int) {
51+
pairs[i], pairs[j] = pairs[j], pairs[i]
52+
})
53+
54+
return []string{
55+
r.Text,
56+
r.HelperText,
57+
pairs[0].text,
58+
pairs[1].text,
59+
pairs[2].text,
60+
pairs[3].text,
61+
pairs[0].audio,
62+
pairs[1].audio,
63+
pairs[2].audio,
64+
pairs[3].audio,
65+
r.AudioAnswer,
66+
}
67+
}
68+
69+
const (
70+
startCloze = "{{c1::"
71+
endCloze = "}}"
72+
)
73+
74+
// CleanedText returns the cleaned text without "{{c1::}}"
75+
func (r Record) CleanedText() string {
76+
start := strings.Index(r.Text, startCloze)
77+
if start == -1 {
78+
return ""
79+
}
80+
81+
end := strings.Index(r.Text[start:], endCloze)
82+
if end == -1 {
83+
return ""
84+
}
85+
86+
// set to absolute end
87+
end = start + end
88+
answer := r.Text[start+len(startCloze) : end]
89+
90+
cleanedText := r.Text[:start] + answer + r.Text[end+len(endCloze):]
91+
cleanedText = strings.ReplaceAll(cleanedText, " ", " ")
92+
if strings.Contains(cleanedText, startCloze) || strings.Contains(cleanedText, endCloze) {
93+
return ""
94+
}
95+
return cleanedText
96+
}
97+
98+
const inputHeaderSize = 6 // only won't include audio fields
99+
100+
// ReadCSVRecords reads records from anki CSV file
101+
func ReadCSVRecords(r io.Reader) ([]Record, error) {
102+
reader := csv.NewReader(r)
103+
reader.TrimLeadingSpace = true
104+
reader.FieldsPerRecord = inputHeaderSize
105+
106+
header, err := reader.Read()
107+
if err != nil {
108+
return nil, fmt.Errorf("%T.Read(): %w", reader, err)
109+
}
110+
h := []string{"Text", "HelperText", "TextA", "TextB", "TextC", "TextD"}
111+
for i := range len(header) {
112+
if header[i] != h[i] {
113+
return nil, fmt.Errorf("invalid header(%q) must be %q", header[i], h[i])
114+
}
115+
}
116+
117+
var records []Record
118+
for {
119+
record, err := reader.Read()
120+
if err == io.EOF {
121+
break
122+
}
123+
if err != nil {
124+
return nil, fmt.Errorf("%T.Read(): %w", reader, err)
125+
}
126+
127+
text := record[0]
128+
if !strings.Contains(text, startCloze) && !strings.Contains(text, endCloze) {
129+
return nil, fmt.Errorf("text(%q) must contain cloze deletion format with %sWORD%s", text, startCloze, endCloze)
130+
}
131+
132+
records = append(records, Record{
133+
Text: record[0],
134+
HelperText: record[1],
135+
TextA: record[2],
136+
TextB: record[3],
137+
TextC: record[4],
138+
TextD: record[5],
139+
})
140+
}
141+
return records, nil
142+
}
143+
144+
// WriteCSVRecords writes records into anki CSV file
145+
func WriteCSVRecords(w io.Writer, records []Record, stripCSVHeader, shuffle bool) error {
146+
writer := csv.NewWriter(w)
147+
148+
if !stripCSVHeader {
149+
h := []string{
150+
"Text", "HelperText", "TextA", "TextB", "TextC", "TextD",
151+
"AudioA", "AudioB", "AudioC", "AudioD", "AudioAnswer",
152+
}
153+
if err := writer.Write(h); err != nil {
154+
return fmt.Errorf("%T.Write(): %w", writer, err)
155+
}
156+
}
157+
158+
for _, r := range records {
159+
var row []string
160+
if shuffle {
161+
row = r.RandomizedRow()
162+
} else {
163+
row = r.Row()
164+
}
165+
166+
if err := writer.Write(row); err != nil {
167+
return fmt.Errorf("%T.Write(): %w", writer, err)
168+
}
169+
}
170+
171+
writer.Flush()
172+
if err := writer.Error(); err != nil {
173+
return fmt.Errorf("%T.Error(): %w", writer, err)
174+
}
175+
return nil
176+
}

0 commit comments

Comments
 (0)