Skip to content

Commit 7c128f9

Browse files
committed
Fix importing Yomichan audio, improve lookup results to stdout
1 parent cd5aabe commit 7c128f9

14 files changed

Lines changed: 350 additions & 129 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/wordbase-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[package]
22
name = "wordbase-cli"
3+
version = "0.0.0"
34

45
authors.workspace = true
56
categories.workspace = true
@@ -27,3 +28,4 @@ tracing = { workspace = true }
2728
tracing-subscriber = { workspace = true, features = ["env-filter"] }
2829
serde = { workspace = true }
2930
serde_json = { workspace = true, features = ["preserve_order"] }
31+
itertools = { workspace = true }

crates/wordbase-cli/src/dict.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ pub async fn import(engine: &Engine, profile: &Profile, path: PathBuf) -> Result
9898
info!("Importing {:?} version {:?}", meta.name, meta.version);
9999
}
100100
ImportEvent::Progress(progress) => {
101-
info!("{:.02}% imported", progress * 100.0);
101+
info!("{:.02}% imported", progress.frac * 100.0);
102102
}
103103
ImportEvent::Done(id) => {
104104
info!("Imported as {id:?}");

crates/wordbase-cli/src/lookup.rs

Lines changed: 134 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,155 @@
1-
use {
2-
anyhow::{Context, Result},
3-
std::{fmt::Write as _, time::Instant},
4-
tracing::info,
5-
wordbase::{Engine, Profile, RecordEntry, RecordKind, render::RenderConfig},
6-
};
1+
use std::{fmt::Write, iter, time::Instant};
72

8-
pub fn deinflect(engine: &Engine, text: &str) {
9-
for deinflect in engine.deinflect(text, 0) {
10-
let text_part = text.get(deinflect.span).unwrap_or("(?)");
11-
info!("{text_part} -> {:?}", deinflect.lemma);
12-
}
13-
}
3+
use anyhow::Result;
4+
use itertools::Itertools;
5+
use tracing::info;
6+
use wordbase::{
7+
DictionaryId, Engine, FrequencyValue, Profile, RecordEntry, RecordKind,
8+
dict::{self, jpn::PitchPosition},
9+
dictionary::Dictionaries,
10+
render,
11+
};
1412

1513
pub async fn lookup(engine: &Engine, profile: &Profile, text: &str) -> Result<Vec<RecordEntry>> {
1614
let start = Instant::now();
17-
let records = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?;
15+
let entries = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?;
1816
let end = Instant::now();
19-
// TODO: a nice, sort-of-human-readable output
17+
18+
let dictionaries = engine.dictionaries();
19+
let mut w = String::new();
20+
for term in render::group_terms(&entries) {
21+
render(&mut w, &dictionaries, &term)?;
22+
}
23+
info!("\n{w}");
24+
2025
info!("Fetched records in {:?}", end.duration_since(start));
21-
Ok(records)
26+
Ok(entries)
2227
}
2328

24-
pub async fn lookup_lemma(engine: &Engine, profile: &Profile, lemma: &str) -> Result<()> {
25-
for result in engine
26-
.lookup_lemma(profile.id, &lemma, RecordKind::ALL)
27-
.await?
28-
{
29-
println!("{result:#?}");
29+
fn render(mut w: impl Write, dictionaries: &Dictionaries, term: &render::RecordTerm) -> Result<()> {
30+
if term.term.reading().is_some() {
31+
let reading = term
32+
.info
33+
.furigana_parts
34+
.iter()
35+
.map(|(headword, reading)| {
36+
if reading.is_empty() {
37+
headword.clone()
38+
} else {
39+
format!("{headword}[{reading}]")
40+
}
41+
})
42+
.join(" ");
43+
writeln!(w, "{} - {reading}", term.term)?;
44+
} else {
45+
writeln!(w, "{}", term.term)?;
3046
}
47+
48+
let tags = iter::empty()
49+
.chain(tags_pitch(term))
50+
.chain(tags_audio(dictionaries, term))
51+
.chain(tags_frequency(dictionaries, term))
52+
.map(|tag| format!("[{tag}]"))
53+
.join(" ");
54+
if !tags.is_empty() {
55+
writeln!(w, " {tags}\n")?;
56+
}
57+
58+
for (&source, glossary_group) in &term.info.glossary_groups {
59+
writeln!(w, " {}:", dict_name(dictionaries, source))?;
60+
61+
for glossary in glossary_group {
62+
for content in &glossary.content {
63+
writeln!(w, " {}", content.chars().take(40).collect::<String>())?;
64+
}
65+
}
66+
}
67+
68+
writeln!(w, "\n")?;
3169
Ok(())
3270
}
3371

34-
pub async fn render(engine: &Engine, profile: &Profile, text: &str) -> Result<()> {
35-
let start = Instant::now();
36-
let records = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?;
37-
let end = Instant::now();
38-
info!("Fetched records in {:?}", end.duration_since(start));
72+
fn tags_frequency<'a>(
73+
dictionaries: &'a Dictionaries,
74+
term: &'a render::RecordTerm,
75+
) -> impl Iterator<Item = String> + 'a {
76+
term.info
77+
.frequencies
78+
.iter()
79+
.map(move |(&source, frequencies)| {
80+
format!(
81+
"{} {}",
82+
dict_name(dictionaries, source),
83+
frequencies
84+
.iter()
85+
.map(|freq| render_frequency(freq))
86+
.join("・")
87+
)
88+
})
89+
}
3990

40-
let start = Instant::now();
41-
let mut html = engine
42-
.render_to_html(
43-
&records,
44-
&RenderConfig {
45-
add_note_text: Some("Add Card".into()),
46-
add_note_js_fn: Some("unimplemented".into()),
47-
},
48-
)
49-
.context("failed to render HTML")?;
50-
_ = write!(&mut html, "<style>{EXTRA_CSS}</style>");
51-
let end = Instant::now();
52-
info!("Rendered HTML in {:?}", end.duration_since(start));
91+
fn render_frequency(frequency: &dict::yomitan::Frequency) -> String {
92+
match (&frequency.display, &frequency.value) {
93+
(Some(display), _) => display.clone(),
94+
(None, Some(FrequencyValue::Rank(rank))) => format!("{rank} ↓"),
95+
(None, Some(FrequencyValue::Occurrence(occurrence))) => format!("{occurrence} ↑"),
96+
(None, None) => "?".into(),
97+
}
98+
}
5399

54-
println!("{html}");
55-
Ok(())
100+
fn tags_pitch(term: &render::RecordTerm) -> impl Iterator<Item = String> {
101+
let morae = term
102+
.term
103+
.reading()
104+
.map(|s| dict::jpn::morae(s).collect::<Vec<_>>());
105+
106+
term.info
107+
.pitches
108+
.iter()
109+
.map(|(position, pitch)| render_pitch(morae.as_deref(), *position, pitch))
110+
.collect::<Vec<_>>()
111+
.into_iter()
56112
}
57113

58-
// TODO: this should probably be put into the renderer somehow
59-
const EXTRA_CSS: &str = "
60-
:root {
61-
--accent-color: #3584e4;
62-
--on-accent-color: #ffffff;
114+
fn render_pitch(morae: Option<&[&str]>, position: PitchPosition, pitch: &render::Pitch) -> String {
115+
let reading = (|| {
116+
let mut morae = morae?.iter().zip(pitch.high.iter()).peekable();
117+
let mut last_high = *morae.peek()?.1;
118+
let mut reading = String::new();
119+
for (mora, &high) in morae {
120+
match (last_high, high) {
121+
(false, false) | (true, true) => {}
122+
(false, true) => {
123+
_ = write!(reading, "/");
124+
}
125+
(true, false) => {
126+
_ = write!(reading, "\");
127+
}
128+
}
129+
last_high = high;
130+
_ = write!(reading, "{mora}");
131+
}
132+
Some(reading)
133+
})();
134+
135+
let reading = reading.unwrap_or_else(|| format!("{}", position.0));
136+
iter::once(reading)
137+
.chain((0..pitch.audio.len()).map(|_| "🔊".to_string()))
138+
.join(" ")
63139
}
64140

65-
:root {
66-
--bg-color: #fafafb;
67-
--fg-color: rgb(0 0 6 / 80%);
141+
fn tags_audio(
142+
dictionaries: &Dictionaries,
143+
term: &render::RecordTerm,
144+
) -> impl Iterator<Item = String> {
145+
term.info
146+
.audio_no_pitch
147+
.iter()
148+
.map(|(&source, _)| format!("🔊 {}", dict_name(dictionaries, source)))
68149
}
69150

70-
@media (prefers-color-scheme: dark) {
71-
:root {
72-
--bg-color: #222226;
73-
--fg-color: #ffffff;
74-
}
151+
fn dict_name(dictionaries: &Dictionaries, id: DictionaryId) -> &str {
152+
dictionaries
153+
.get(&id)
154+
.map_or("?", |dict| dict.meta.name.as_str())
75155
}
76-
";

crates/wordbase-cli/src/main.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod dict;
55
mod lookup;
66
mod profile;
7+
mod query;
78

89
use {
910
anyhow::{Context, Result, bail},
@@ -241,15 +242,16 @@ async fn main() -> Result<()> {
241242
args.output,
242243
lookup::lookup(&engine, &*require_profile()?, &text).await?,
243244
),
245+
// query
244246
Command::LookupLemma { lemma } => output(
245247
args.output,
246-
lookup::lookup_lemma(&engine, &*require_profile()?, &lemma).await?,
248+
query::lookup_lemma(&engine, &*require_profile()?, &lemma).await?,
247249
),
248250
Command::Render { text } => {
249-
lookup::render(&engine, &*require_profile()?, &text).await?;
251+
query::render(&engine, &*require_profile()?, &text).await?;
250252
}
251253
Command::Deinflect { text } => {
252-
lookup::deinflect(&engine, &text);
254+
query::deinflect(&engine, &text);
253255
}
254256
// profile
255257
Command::Profile {

crates/wordbase-cli/src/query.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use {
2+
anyhow::{Context, Result},
3+
std::{fmt::Write as _, time::Instant},
4+
tracing::info,
5+
wordbase::{Engine, Profile, RecordKind, render::RenderConfig},
6+
};
7+
8+
pub fn deinflect(engine: &Engine, text: &str) {
9+
for deinflect in engine.deinflect(text, 0) {
10+
let text_part = text.get(deinflect.span).unwrap_or("(?)");
11+
info!("{text_part} -> {:?}", deinflect.lemma);
12+
}
13+
}
14+
15+
pub async fn lookup_lemma(engine: &Engine, profile: &Profile, lemma: &str) -> Result<()> {
16+
for result in engine
17+
.lookup_lemma(profile.id, &lemma, RecordKind::ALL)
18+
.await?
19+
{
20+
println!("{result:#?}");
21+
}
22+
Ok(())
23+
}
24+
25+
pub async fn render(engine: &Engine, profile: &Profile, text: &str) -> Result<()> {
26+
let start = Instant::now();
27+
let records = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?;
28+
let end = Instant::now();
29+
info!("Fetched records in {:?}", end.duration_since(start));
30+
31+
let start = Instant::now();
32+
let mut html = engine
33+
.render_to_html(
34+
&records,
35+
&RenderConfig {
36+
add_note_text: Some("Add Card".into()),
37+
add_note_js_fn: Some("unimplemented".into()),
38+
},
39+
)
40+
.context("failed to render HTML")?;
41+
_ = write!(&mut html, "<style>{EXTRA_CSS}</style>");
42+
let end = Instant::now();
43+
info!("Rendered HTML in {:?}", end.duration_since(start));
44+
45+
println!("{html}");
46+
Ok(())
47+
}
48+
49+
// TODO: this should probably be put into the renderer somehow
50+
const EXTRA_CSS: &str = "
51+
:root {
52+
--accent-color: #3584e4;
53+
--on-accent-color: #ffffff;
54+
}
55+
56+
:root {
57+
--bg-color: #fafafb;
58+
--fg-color: rgb(0 0 6 / 80%);
59+
}
60+
61+
@media (prefers-color-scheme: dark) {
62+
:root {
63+
--bg-color: #222226;
64+
--fg-color: #ffffff;
65+
}
66+
}
67+
";

crates/wordbase/src/dictionary.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,26 @@ impl Engine {
5353

5454
let mut tx = conn.begin().await.context("failed to begin transaction")?;
5555

56-
info!("Deleting {id:?}");
56+
info!("Deleting term records for {id:?}");
5757
let start = Instant::now();
5858

5959
sqlx::query!("DELETE FROM term_record WHERE source = $1", id.0)
6060
.execute(&mut *tx)
6161
.await
6262
.context("failed to delete term records")?;
63-
info!("Deleted term records");
63+
info!("Deleting records");
6464

6565
sqlx::query!("DELETE FROM record WHERE source = $1", id.0)
6666
.execute(&mut *tx)
6767
.await
6868
.context("failed to delete records")?;
69-
info!("Deleted records");
69+
info!("Deleting frequency records");
7070

7171
sqlx::query!("DELETE FROM frequency WHERE source = $1", id.0)
7272
.execute(&mut *tx)
7373
.await
7474
.context("failed to delete frequency rows")?;
75-
info!("Deleted frequency records");
75+
info!("Deleting dictionary record");
7676

7777
let result = sqlx::query!("DELETE FROM dictionary WHERE id = $1", id.0)
7878
.execute(&mut *tx)
@@ -81,10 +81,10 @@ impl Engine {
8181
if result.rows_affected() == 0 {
8282
bail!(NotFound);
8383
}
84-
info!("Deleted dictionary record");
84+
info!("Committing");
8585

8686
tx.commit().await.context("failed to commit transaction")?;
87-
info!("Committed");
87+
info!("Vacuuming");
8888

8989
sqlx::query!("VACUUM")
9090
.execute(&self.db)

0 commit comments

Comments
 (0)