1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! `furigana lookup` サブコマンド
//!
//! 1 回だけ変換してそれを stdout に出して終了する CLI。
//! サーバー起動なし、即時 1 ショット用途。
//! 公開 API の `mode` パラメータと同じ 4 種に対応。
use crate::config::Config;
use crate::paths::Paths;
use anyhow::{bail, Context, Result};
use clap::Args as ClapArgs;
use furigana::{Furigana, RomajiStyle, TtsOptions};
use std::path::PathBuf;
/// `furigana lookup` のオプション
#[derive(ClapArgs, Debug)]
pub struct Args {
/// 変換対象テキスト (`--batch` 指定時は省略する)
text: Option<String>,
/// stdin を 1 行 1 入力として読み、 1 行 1 結果を stdout に出す。
/// 辞書の load が 1 回で済むので、 多数の入力をまとめて変換する時に使う
/// (回帰テスト等)。 出力が複数行になる mode (`analyze` / `accent` /
/// `--max-len` 付きの `aquestalk`) は行対応が壊れるので受け付けない。
#[arg(long)]
batch: bool,
/// 変換モード: `tts` (default) | `hiragana` | `ruby` | `kanji` | `romaji` | `romaji-kunrei` |
/// `analyze` | `accent` | `voicevox-aques` | `aquestalk` | `bouyomi`
#[arg(short, long, default_value = "tts")]
mode: String,
/// dev / test 用: rules dir を直接指定 (= furigana-dict/rules/)。
/// 指定時は `--data-dir/data/` スキャンを skip して raw dict 構造から直接 load する。
/// `furigana dict pull` 配置済の通常運用では指定不要。
#[arg(long)]
rules_dir: Option<PathBuf>,
/// dev / test 用: core dict dir を直接指定 (複数指定可、
/// = furigana-dict/core/{jukugo, unihan, kanji, loanwords, works} 等)。
/// 指定時は `--data-dir/data/` スキャンを skip する。
#[arg(long)]
core_dict_dir: Vec<PathBuf>,
/// TTS: 「、」後に挿入する文字列
#[arg(long, default_value = " ")]
short_pause: String,
/// TTS: 「。!?」後に挿入する文字列
#[arg(long, default_value = " ")]
long_pause: String,
/// TTS: `。` を残さず削除する
#[arg(long)]
drop_period: bool,
/// TTS: 絵文字 / 顔文字パーツを読み上げから外す
#[arg(long)]
silence_symbols: bool,
/// aquestalk: 無声化記号 `_` の自動付与を無効にする
#[arg(long)]
no_devoice: bool,
/// aquestalk: 記号列をこの文字数以下のアクセント句単位へ分割し 1 行 1 塊で出力する
/// (エンジン側の長さ上限対策。 0 = 分割しない)
#[arg(long, default_value_t = 0)]
max_len: usize,
/// accent/analyze: dict bracket が無い token にも rule-based accent 推定を適用する
/// (ADR-0007。 推定 phrase は `"estimated": true` で真値と区別される)
#[arg(long)]
estimate_accent: bool,
/// accent/analyze: dict のアクセント専用の表 (role = "accent") を読まない
#[arg(long)]
no_accent_lexicon: bool,
/// dict のこの相対 path (dir なら配下全部、 file なら 1 file) を読まない。 複数回指定可
/// (例: `--exclude-dict works --exclude-dict jukugo/society/mahjong`)
#[arg(long = "exclude-dict", value_name = "PATH")]
exclude_dict: Vec<String>,
}
/// 実行
pub fn run(args: &Args, paths: &Paths, _cfg: &Config) -> Result<()> {
if args.batch && args.text.is_some() {
bail!("--batch と text 引数は同時に指定できません (入力は stdin から読みます)");
}
if args.batch && matches!(args.mode.as_str(), "analyze" | "accent") {
bail!(
"--batch は mode `{}` では使えません (出力が複数行になるため)",
args.mode
);
}
if args.batch && args.mode == "aquestalk" && args.max_len != 0 {
bail!("--batch は `aquestalk` + `--max-len` では使えません (出力が複数行になるため)");
}
let f = if args.rules_dir.is_some() || !args.core_dict_dir.is_empty() {
// dev/test override: raw furigana-dict/ 構造 (rules/ + core/<sub>/) から直接 load。
// build_furigana の `<data_dir>/data/` flat スキャンを bypass して dev workflow を支える。
let mut b = Furigana::builder();
if let Some(rules) = &args.rules_dir {
b = b.rules_dir(rules);
}
for core in &args.core_dict_dir {
b = b.core_dict_dir(core);
}
super::apply_dict_excludes(b, &args.exclude_dict)
.estimate_accent(args.estimate_accent)
.accent_lexicon(!args.no_accent_lexicon)
.build()?
} else {
super::apply_dict_excludes(super::furigana_builder(paths), &args.exclude_dict)
.estimate_accent(args.estimate_accent)
.accent_lexicon(!args.no_accent_lexicon)
.build()?
};
if args.batch {
use std::io::{BufRead, BufWriter, Write};
let stdin = std::io::stdin();
let mut out = BufWriter::new(std::io::stdout().lock());
for line in stdin.lock().lines() {
let line = line.context("read stdin")?;
writeln!(out, "{}", convert(&f, &line, args)?).context("write stdout")?;
}
out.flush().context("flush stdout")?;
return Ok(());
}
let text = args
.text
.clone()
.context("変換対象テキストを指定してください (または --batch で stdin から読みます)")?;
println!("{}", convert(&f, &text, args)?);
Ok(())
}
/// 1 入力を mode に応じて変換する。 `lookup` と `--batch` で共有する。
fn convert(f: &Furigana, text: &str, args: &Args) -> Result<String> {
let result = match args.mode.as_str() {
"kanji" => text.to_string(),
"ruby" => f.to_ruby(text),
"hiragana" | "hira" => f.to_hiragana(text),
"romaji" => f.to_romaji(text, RomajiStyle::Hepburn),
"romaji-kunrei" | "kunrei" => f.to_romaji(text, RomajiStyle::Kunrei),
// 棒読みちゃん (互換サーバー含む) へ流すテキストは tts mode と同一
// (= 読み化 + pause 整形。 棒読みちゃん側の漢字誤読を bypass する)
"tts" | "bouyomi" => {
let opts = TtsOptions::default()
.with_short_pause(args.short_pause.clone())
.with_long_pause(args.long_pause.clone())
.with_keep_period(!args.drop_period)
.with_silence_symbols(args.silence_symbols);
f.to_tts(text, &opts)
}
// VOICEVOX AquesTalk-風記法 (ADR-0001 adapter crate 経由)。
// POST /accent_phrases?is_kana=true にそのまま渡せる。
// dict bracket / --estimate-accent が無い token は平板 fallback。
"voicevox-aques" | "voicevox" => {
ja_furigana_voicevox::to_aques_kana(&f.to_accent(text))
}
// 本家 AquesTalk 音声記号列 (ADR-0001 adapter crate 経由)。
// AquesTalk2 / AquesTalk10 の合成 API へそのまま渡せる。
// VOICEVOX kana 記法との差分 = 半角 `?` / `。` と `、` の pause 区別 / 無声化 `_`。
"aquestalk" => {
let symbols = ja_furigana_aquestalk::to_aquestalk_with(
&f.to_accent(text),
ja_furigana_aquestalk::Options {
devoice: !args.no_devoice,
trailing_period: !args.drop_period,
},
);
if args.max_len == 0 {
symbols
} else {
// 1 行 1 塊 = そのまま逐次合成に流せる
ja_furigana_aquestalk::split_for_aquestalk(&symbols, args.max_len).join("\n")
}
}
// Smart engine debug API (★F1): AnalyzeResult を JSON pretty 出力。
// alpha.10 段階の experimental、 path 採択 / 候補列 / boundary region を inspect 用途。
"analyze" => {
let result = f.analyze(text);
serde_json::to_string_pretty(&result).context("serialize AnalyzeResult to JSON")?
}
"accent" => {
let result = f.to_accent(text);
serde_json::to_string_pretty(&result).context("serialize AccentResult to JSON")?
}
other => bail!(
"未知の mode: {other} (使用可能: tts | bouyomi | hiragana | ruby | kanji | romaji | romaji-kunrei | analyze | accent | voicevox-aques | aquestalk)"
),
};
Ok(result)
}