hersona 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hersona/__init__.py +10 -0
- hersona/cli/__init__.py +9 -0
- hersona/cli/__main__.py +7 -0
- hersona/cli/app.py +434 -0
- hersona/core/__init__.py +105 -0
- hersona/core/attach.py +271 -0
- hersona/core/authoring.py +317 -0
- hersona/core/compatibility.py +250 -0
- hersona/core/constants.py +18 -0
- hersona/core/i18n.py +171 -0
- hersona/core/intensity.py +296 -0
- hersona/core/paths.py +32 -0
- hersona/core/recommend.py +505 -0
- hersona/core/weight.py +144 -0
- hersona/data/attributes/archetype/childhood_friend.yaml +25 -0
- hersona/data/attributes/archetype/gamer_otaku.yaml +25 -0
- hersona/data/attributes/archetype/heroine.yaml +25 -0
- hersona/data/attributes/archetype/hikikomori.yaml +61 -0
- hersona/data/attributes/archetype/idol.yaml +61 -0
- hersona/data/attributes/archetype/mentor.yaml +25 -0
- hersona/data/attributes/archetype/rival.yaml +25 -0
- hersona/data/attributes/archetype/robot_android.yaml +27 -0
- hersona/data/attributes/archetype/shrine_maiden.yaml +27 -0
- hersona/data/attributes/hobby/cooking.yaml +54 -0
- hersona/data/attributes/hobby/gamer.yaml +54 -0
- hersona/data/attributes/hobby/music.yaml +54 -0
- hersona/data/attributes/hobby/reading.yaml +55 -0
- hersona/data/attributes/hobby/sports.yaml +55 -0
- hersona/data/attributes/personality/airhead.yaml +129 -0
- hersona/data/attributes/personality/chuunibyou.yaml +129 -0
- hersona/data/attributes/personality/dandere.yaml +126 -0
- hersona/data/attributes/personality/genki.yaml +127 -0
- hersona/data/attributes/personality/hot_blooded.yaml +130 -0
- hersona/data/attributes/personality/intellectual.yaml +129 -0
- hersona/data/attributes/personality/klutz.yaml +128 -0
- hersona/data/attributes/personality/kuudere.yaml +127 -0
- hersona/data/attributes/personality/mysterious.yaml +130 -0
- hersona/data/attributes/personality/narcissist.yaml +129 -0
- hersona/data/attributes/personality/optimist.yaml +127 -0
- hersona/data/attributes/personality/pessimist.yaml +126 -0
- hersona/data/attributes/personality/playful.yaml +127 -0
- hersona/data/attributes/personality/pragmatist.yaml +130 -0
- hersona/data/attributes/personality/protective.yaml +128 -0
- hersona/data/attributes/personality/serious.yaml +126 -0
- hersona/data/attributes/personality/stoic.yaml +126 -0
- hersona/data/attributes/personality/switch.yaml +127 -0
- hersona/data/attributes/personality/tsundere.yaml +124 -0
- hersona/data/attributes/personality/yandere.yaml +127 -0
- hersona/data/attributes/speech/archaic.yaml +29 -0
- hersona/data/attributes/speech/blunt.yaml +48 -0
- hersona/data/attributes/speech/blunt_en.yaml +36 -0
- hersona/data/attributes/speech/boku_girl.yaml +29 -0
- hersona/data/attributes/speech/british_en.yaml +41 -0
- hersona/data/attributes/speech/casual_en.yaml +38 -0
- hersona/data/attributes/speech/formal_en.yaml +39 -0
- hersona/data/attributes/speech/gyaru.yaml +49 -0
- hersona/data/attributes/speech/kansai_ben.yaml +29 -0
- hersona/data/attributes/speech/keigo.yaml +30 -0
- hersona/data/attributes/speech/kyoto_ben.yaml +67 -0
- hersona/data/attributes/speech/mischievous.yaml +47 -0
- hersona/data/attributes/speech/mixed_dialect.yaml +47 -0
- hersona/data/attributes/speech/onee_kotoba.yaml +29 -0
- hersona/data/attributes/speech/ore_boy.yaml +30 -0
- hersona/data/attributes/speech/princess_speech.yaml +48 -0
- hersona/data/attributes/speech/seductive.yaml +50 -0
- hersona/data/attributes/speech/soft.yaml +48 -0
- hersona/data/attributes/speech/southern_us_en.yaml +41 -0
- hersona/data/attributes/speech/stutter.yaml +43 -0
- hersona/data/attributes/speech/theatrical.yaml +47 -0
- hersona/data/attributes/speech/third_person.yaml +29 -0
- hersona/data/attributes/speech/tomboy.yaml +49 -0
- hersona/data/attributes/speech/washi.yaml +64 -0
- hersona/data/attributes/speech/whispery.yaml +29 -0
- hersona/data/attributes/visual/animal_ears.yaml +55 -0
- hersona/data/attributes/visual/glamorous.yaml +54 -0
- hersona/data/attributes/visual/glasses.yaml +55 -0
- hersona/data/attributes/visual/petite.yaml +54 -0
- hersona/data/attributes/visual/silver_hair.yaml +56 -0
- hersona/data/quiz/recommend_quiz.en.yaml +400 -0
- hersona/data/quiz/recommend_quiz.yaml +257 -0
- hersona/data/schema/attribute.schema.json +225 -0
- hersona/locales/en.yaml +116 -0
- hersona/locales/ja.yaml +114 -0
- hersona-0.0.1.dist-info/METADATA +214 -0
- hersona-0.0.1.dist-info/RECORD +89 -0
- hersona-0.0.1.dist-info/WHEEL +4 -0
- hersona-0.0.1.dist-info/entry_points.txt +2 -0
- hersona-0.0.1.dist-info/licenses/LICENSE +22 -0
- hersona-0.0.1.dist-info/licenses/LICENSE-CC0.txt +73 -0
hersona/__init__.py
ADDED
hersona/cli/__init__.py
ADDED
hersona/cli/__main__.py
ADDED
hersona/cli/app.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""hersona CLI 本体 (argparse)。
|
|
2
|
+
|
|
3
|
+
サブコマンド:
|
|
4
|
+
hersona list 利用可能な属性を一覧
|
|
5
|
+
hersona show <name> 属性の詳細
|
|
6
|
+
hersona matrix [--json] 相性マトリクスをダンプ
|
|
7
|
+
hersona blend <name> [<name>...] 属性をブレンドしてプロンプト注入ブロックを表示
|
|
8
|
+
hersona recommend [--answers ...] 診断クイズ → 推薦 (→ --apply で注入ブロック)
|
|
9
|
+
hersona create [...] 属性を作成しユーザー名前空間に保存
|
|
10
|
+
hersona measure <name>... 出力テキストの強度指標を採点 (speech 属性必須)
|
|
11
|
+
|
|
12
|
+
対話入力を伴うコマンド (recommend / create) は、フラグで全入力を与えると
|
|
13
|
+
非対話で実行できる (スクリプト / テスト用)。
|
|
14
|
+
|
|
15
|
+
UI 文言は ``hersona/locales/<lang>.yaml`` のカタログに外部化し、``i18n.tr`` で
|
|
16
|
+
参照する。表示言語は ``--lang`` / ``HERSONA_LANG`` / 既定 en で決まる。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
from hersona.core.attach import available_attributes, load_attribute, render_blend
|
|
26
|
+
from hersona.core.authoring import (
|
|
27
|
+
AuthoringError,
|
|
28
|
+
build_attribute,
|
|
29
|
+
save_attribute,
|
|
30
|
+
user_attributes_root,
|
|
31
|
+
)
|
|
32
|
+
from hersona.core.compatibility import load_matrix
|
|
33
|
+
from hersona.core.constants import CATEGORY_ORDER
|
|
34
|
+
from hersona.core.i18n import SUPPORTED_LANGS, resolve_meta, set_active_lang, tr
|
|
35
|
+
from hersona.core.intensity import content_language, format_report
|
|
36
|
+
from hersona.core.intensity import skip_reason as intensity_skip_reason
|
|
37
|
+
from hersona.core.intensity import verify as verify_intensity
|
|
38
|
+
from hersona.core.recommend import quiz_for_lang, recommend
|
|
39
|
+
from hersona.core.weight import WeightLevel
|
|
40
|
+
|
|
41
|
+
_WEIGHT_CHOICES = [w.value for w in WeightLevel]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list[str] | None = None) -> int:
|
|
45
|
+
# 表示言語を最初に確定する (設計書 §3.1): --lang > HERSONA_LANG > 既定 en。
|
|
46
|
+
# argparse の help/description もローカライズするため、パーサ構築前に決める。
|
|
47
|
+
raw = sys.argv[1:] if argv is None else argv
|
|
48
|
+
lang = set_active_lang(_peek_lang(raw))
|
|
49
|
+
parser = _build_parser()
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
args.lang = lang
|
|
52
|
+
handler: Callable[[argparse.Namespace], int] | None = getattr(args, "_handler", None)
|
|
53
|
+
if handler is None:
|
|
54
|
+
parser.print_help()
|
|
55
|
+
return 0
|
|
56
|
+
try:
|
|
57
|
+
return handler(args)
|
|
58
|
+
except (AuthoringError, KeyError, ValueError) as e:
|
|
59
|
+
print(f"{tr('error.prefix')}{e}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _peek_lang(argv: list[str]) -> str | None:
|
|
64
|
+
"""パーサ構築前に ``--lang`` の値を先読みする。
|
|
65
|
+
|
|
66
|
+
``--lang ja`` / ``--lang=ja`` の双方に対応。未指定なら None (env/既定へ委譲)。
|
|
67
|
+
"""
|
|
68
|
+
for i, token in enumerate(argv):
|
|
69
|
+
if token == "--lang" and i + 1 < len(argv):
|
|
70
|
+
return argv[i + 1]
|
|
71
|
+
if token.startswith("--lang="):
|
|
72
|
+
return token.split("=", 1)[1]
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _lang_parser() -> argparse.ArgumentParser:
|
|
77
|
+
"""全サブコマンドで共有する ``--lang`` 親パーサ。
|
|
78
|
+
|
|
79
|
+
``hersona --lang ja list`` (前置) と ``hersona list --lang ja`` (後置) の
|
|
80
|
+
両方を受理できるよう、トップレベルと各サブパーサの双方に付与する。
|
|
81
|
+
"""
|
|
82
|
+
p = argparse.ArgumentParser(add_help=False)
|
|
83
|
+
# default=SUPPRESS: 親 (top-level) と子 (subparser) の双方に同じ --lang を
|
|
84
|
+
# 持たせると、子の既定値 None が前置指定 (`hersona --lang ja list`) で
|
|
85
|
+
# 解決済みの値を上書きしてしまう。SUPPRESS で「明示時のみ namespace に載る」
|
|
86
|
+
# 挙動にし、どちらの位置で指定しても他方を潰さないようにする。
|
|
87
|
+
p.add_argument(
|
|
88
|
+
"--lang",
|
|
89
|
+
choices=list(SUPPORTED_LANGS),
|
|
90
|
+
default=argparse.SUPPRESS,
|
|
91
|
+
help=tr("cli.lang_help"),
|
|
92
|
+
)
|
|
93
|
+
return p
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
97
|
+
lang_opt = _lang_parser()
|
|
98
|
+
parser = argparse.ArgumentParser(
|
|
99
|
+
prog="hersona", description=tr("cli.description"), parents=[lang_opt]
|
|
100
|
+
)
|
|
101
|
+
sub = parser.add_subparsers(dest="command")
|
|
102
|
+
|
|
103
|
+
def add(name: str, **kw: object) -> argparse.ArgumentParser:
|
|
104
|
+
# 全サブコマンドに --lang を継承させる薄いラッパ。
|
|
105
|
+
return sub.add_parser(name, parents=[lang_opt], **kw)
|
|
106
|
+
|
|
107
|
+
p_list = add("list", help=tr("help.list"))
|
|
108
|
+
p_list.set_defaults(_handler=_cmd_list)
|
|
109
|
+
|
|
110
|
+
p_show = add("show", help=tr("help.show"))
|
|
111
|
+
p_show.add_argument("name", help=tr("help.show_name"))
|
|
112
|
+
p_show.set_defaults(_handler=_cmd_show)
|
|
113
|
+
|
|
114
|
+
p_matrix = add("matrix", help=tr("help.matrix"))
|
|
115
|
+
p_matrix.add_argument("--json", action="store_true", help=tr("help.json"))
|
|
116
|
+
p_matrix.set_defaults(_handler=_cmd_matrix)
|
|
117
|
+
|
|
118
|
+
p_blend = add("blend", help=tr("help.blend"))
|
|
119
|
+
p_blend.add_argument("names", nargs="+", help=tr("help.names"))
|
|
120
|
+
p_blend.add_argument(
|
|
121
|
+
"--weight", choices=_WEIGHT_CHOICES, default="moderate", help=tr("help.weight_blend")
|
|
122
|
+
)
|
|
123
|
+
p_blend.set_defaults(_handler=_cmd_blend)
|
|
124
|
+
|
|
125
|
+
p_rec = add("recommend", help=tr("help.recommend"))
|
|
126
|
+
p_rec.add_argument("--answers", help=tr("help.rec_answers"))
|
|
127
|
+
p_rec.add_argument("--apply", action="store_true", help=tr("help.rec_apply"))
|
|
128
|
+
p_rec.add_argument("--weight", choices=_WEIGHT_CHOICES, help=tr("help.rec_weight"))
|
|
129
|
+
p_rec.add_argument("--explain", action="store_true", help=tr("help.rec_explain"))
|
|
130
|
+
p_rec.add_argument("--top", type=int, default=1, help=tr("help.rec_top"))
|
|
131
|
+
p_rec.add_argument("--json", action="store_true", help=tr("help.json"))
|
|
132
|
+
p_rec.set_defaults(_handler=_cmd_recommend)
|
|
133
|
+
|
|
134
|
+
p_create = add("create", help=tr("help.create"))
|
|
135
|
+
p_create.add_argument("--category", choices=list(CATEGORY_ORDER))
|
|
136
|
+
p_create.add_argument("--name")
|
|
137
|
+
p_create.add_argument("--display-ja")
|
|
138
|
+
p_create.add_argument("--display-en")
|
|
139
|
+
p_create.add_argument(
|
|
140
|
+
"--weight", choices=["none", "mild", "moderate", "strong"], default="moderate"
|
|
141
|
+
)
|
|
142
|
+
p_create.add_argument("--desc-ja")
|
|
143
|
+
p_create.add_argument("--desc-en")
|
|
144
|
+
p_create.add_argument("--example", action="append", dest="examples", help=tr("help.create_example"))
|
|
145
|
+
p_create.add_argument("--overwrite", action="store_true")
|
|
146
|
+
p_create.set_defaults(_handler=_cmd_create)
|
|
147
|
+
|
|
148
|
+
p_measure = add("measure", help=tr("help.measure"))
|
|
149
|
+
p_measure.add_argument("names", nargs="+", help=tr("help.names"))
|
|
150
|
+
p_measure.add_argument(
|
|
151
|
+
"--weight", choices=_WEIGHT_CHOICES, default="moderate", help=tr("help.weight_measure")
|
|
152
|
+
)
|
|
153
|
+
p_measure.add_argument("--input", help=tr("help.measure_input"))
|
|
154
|
+
p_measure.add_argument("--text", help=tr("help.measure_text"))
|
|
155
|
+
p_measure.set_defaults(_handler=_cmd_measure)
|
|
156
|
+
|
|
157
|
+
return parser
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _normalize_name(name: str) -> str:
|
|
161
|
+
"""'<category>/<name>' 形式なら name 部分を返す。"""
|
|
162
|
+
return name.split("/", 1)[1] if "/" in name else name
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
166
|
+
attrs = available_attributes()
|
|
167
|
+
by_cat: dict[str, list[tuple[str, str]]] = {}
|
|
168
|
+
for name, meta in sorted(attrs.items()):
|
|
169
|
+
by_cat.setdefault(meta["category"], []).append((name, meta["source"]))
|
|
170
|
+
print(tr("list.header", count=len(attrs)))
|
|
171
|
+
for cat in CATEGORY_ORDER:
|
|
172
|
+
items = by_cat.get(cat, [])
|
|
173
|
+
if not items:
|
|
174
|
+
continue
|
|
175
|
+
print("\n" + tr("list.category", category=cat, count=len(items)))
|
|
176
|
+
for name, source in items:
|
|
177
|
+
tag = tr("list.user_tag") if source == "user" else ""
|
|
178
|
+
print(f" - {name}{tag}")
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _cmd_show(args: argparse.Namespace) -> int:
|
|
183
|
+
data = load_attribute(_normalize_name(args.name))
|
|
184
|
+
print(f"=== {data['attribute_category']}/{data['attribute_name']} ===")
|
|
185
|
+
display_name = resolve_meta(data, "display_name")
|
|
186
|
+
if display_name:
|
|
187
|
+
print(f"display_name: {display_name}")
|
|
188
|
+
description = resolve_meta(data, "description")
|
|
189
|
+
if description:
|
|
190
|
+
print(f"description: {description}")
|
|
191
|
+
for key in ("weight_dimension", "typical_value_range"):
|
|
192
|
+
if data.get(key):
|
|
193
|
+
print(f"{key}: {data[key]}")
|
|
194
|
+
for key in ("core_traits", "catchphrases", "sentence_endings"):
|
|
195
|
+
if data.get(key):
|
|
196
|
+
print(f"{key}: {len(data[key])} ({', '.join(data[key][:3])} ...)")
|
|
197
|
+
for key in ("second_person", "tone"):
|
|
198
|
+
if data.get(key):
|
|
199
|
+
print(f"{key}: {data[key]}")
|
|
200
|
+
if data.get("compatible_archetypes"):
|
|
201
|
+
print(f"compatible_archetypes: {data['compatible_archetypes']}")
|
|
202
|
+
if data.get("conflicts_with"):
|
|
203
|
+
print(f"conflicts_with: {data['conflicts_with']}")
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _cmd_matrix(args: argparse.Namespace) -> int:
|
|
208
|
+
matrix = load_matrix()
|
|
209
|
+
if args.json:
|
|
210
|
+
print(json.dumps(matrix.to_dict(), ensure_ascii=False, indent=2))
|
|
211
|
+
return 0
|
|
212
|
+
for name in matrix.names():
|
|
213
|
+
conf = sorted(matrix.conflicts_of(name))
|
|
214
|
+
comp = sorted(matrix.compatible_of(name))
|
|
215
|
+
print(f"{name}: conflicts={conf} compatible={comp}")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _cmd_blend(args: argparse.Namespace) -> int:
|
|
220
|
+
names = [_normalize_name(n) for n in args.names]
|
|
221
|
+
result = render_blend(names, weight=args.weight)
|
|
222
|
+
if result.conflicts:
|
|
223
|
+
print(tr("blend.conflict", conflicts=result.conflicts), file=sys.stderr)
|
|
224
|
+
print(result.prompt)
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _parse_answers(raw: str) -> dict[str, int]:
|
|
229
|
+
answers: dict[str, int] = {}
|
|
230
|
+
for token in raw.split(","):
|
|
231
|
+
token = token.strip()
|
|
232
|
+
if not token:
|
|
233
|
+
continue
|
|
234
|
+
qid, _, idx = token.partition("=")
|
|
235
|
+
answers[qid.strip()] = int(idx)
|
|
236
|
+
return answers
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _cmd_recommend(args: argparse.Namespace) -> int:
|
|
240
|
+
# 表示言語に応じた既定クイズ (W2: en は英語 speech へ導線するロケール別クイズ)
|
|
241
|
+
quiz = quiz_for_lang()
|
|
242
|
+
if args.answers:
|
|
243
|
+
answers = _parse_answers(args.answers)
|
|
244
|
+
else:
|
|
245
|
+
answers = _interactive_quiz(quiz)
|
|
246
|
+
|
|
247
|
+
top = getattr(args, "top", 1) or 1
|
|
248
|
+
rec = recommend(answers, top=top, quiz=quiz)
|
|
249
|
+
if args.json:
|
|
250
|
+
print(
|
|
251
|
+
json.dumps(
|
|
252
|
+
{
|
|
253
|
+
"blend": rec.blend,
|
|
254
|
+
"candidates": rec.candidates,
|
|
255
|
+
"scores": rec.scores,
|
|
256
|
+
"dropped": rec.dropped,
|
|
257
|
+
"rationale": rec.rationale,
|
|
258
|
+
"alternatives": [
|
|
259
|
+
{"dropped": d, "alternative": a, "score": s}
|
|
260
|
+
for d, a, s in rec.alternatives
|
|
261
|
+
],
|
|
262
|
+
"weight_suggestion": rec.weight_suggestion.value,
|
|
263
|
+
"summary": rec.summary(),
|
|
264
|
+
},
|
|
265
|
+
ensure_ascii=False,
|
|
266
|
+
indent=2,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
print(tr("recommend.header"))
|
|
272
|
+
blend = " + ".join(rec.blend) if rec.blend else tr("common.none")
|
|
273
|
+
print(tr("recommend.blend", blend=blend))
|
|
274
|
+
print(tr("recommend.summary", summary=rec.summary()))
|
|
275
|
+
print(tr("recommend.weight", weight=rec.weight_suggestion.value))
|
|
276
|
+
top = rec.ranked()[:5]
|
|
277
|
+
if top:
|
|
278
|
+
items = ", ".join(f"{n}({s:g})" for n, s in top)
|
|
279
|
+
print(tr("recommend.top", items=items))
|
|
280
|
+
for name, reason in rec.dropped:
|
|
281
|
+
print(tr("recommend.dropped", name=name, reason=reason))
|
|
282
|
+
|
|
283
|
+
if args.explain:
|
|
284
|
+
print("\n" + tr("recommend.rationale_header"))
|
|
285
|
+
for name in rec.blend:
|
|
286
|
+
reasons = rec.rationale.get(name, [])
|
|
287
|
+
print(f" {name}:")
|
|
288
|
+
for r in reasons:
|
|
289
|
+
print(f" - {r}")
|
|
290
|
+
if rec.alternatives:
|
|
291
|
+
print("\n" + tr("recommend.alt_header"))
|
|
292
|
+
for dropped, alt, score in rec.alternatives:
|
|
293
|
+
print(tr("recommend.alt_item", dropped=dropped, alt=alt, score=f"{score:g}"))
|
|
294
|
+
|
|
295
|
+
if args.apply and rec.blend:
|
|
296
|
+
weight = args.weight or rec.weight_suggestion.value
|
|
297
|
+
print("\n" + tr("recommend.inject_header", weight=weight))
|
|
298
|
+
print(render_blend(rec.blend, weight=weight).prompt)
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _interactive_quiz(quiz) -> dict[str, int]:
|
|
303
|
+
answers: dict[str, int] = {}
|
|
304
|
+
for q in quiz:
|
|
305
|
+
print(f"\n{q.localized_prompt()}")
|
|
306
|
+
for i, opt in enumerate(q.options):
|
|
307
|
+
print(f" [{i}] {opt.localized_label()}")
|
|
308
|
+
while True:
|
|
309
|
+
raw = input(tr("quiz.prompt_select")).strip()
|
|
310
|
+
try:
|
|
311
|
+
idx = int(raw)
|
|
312
|
+
if 0 <= idx < len(q.options):
|
|
313
|
+
answers[q.id] = idx
|
|
314
|
+
break
|
|
315
|
+
except ValueError:
|
|
316
|
+
pass
|
|
317
|
+
print(tr("quiz.invalid_number"))
|
|
318
|
+
return answers
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _cmd_create(args: argparse.Namespace) -> int:
|
|
322
|
+
if args.name and args.category:
|
|
323
|
+
data = _create_from_flags(args)
|
|
324
|
+
else:
|
|
325
|
+
data = _interactive_create()
|
|
326
|
+
dest = save_attribute(data, overwrite=args.overwrite)
|
|
327
|
+
print(tr("create.saved", dest=dest))
|
|
328
|
+
print(tr("create.namespace", root=user_attributes_root()))
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _create_from_flags(args: argparse.Namespace) -> dict:
|
|
333
|
+
missing = [
|
|
334
|
+
flag
|
|
335
|
+
for flag, val in {
|
|
336
|
+
"--display-ja": args.display_ja,
|
|
337
|
+
"--display-en": args.display_en,
|
|
338
|
+
"--desc-ja": args.desc_ja,
|
|
339
|
+
"--desc-en": args.desc_en,
|
|
340
|
+
"--example": args.examples,
|
|
341
|
+
}.items()
|
|
342
|
+
if not val
|
|
343
|
+
]
|
|
344
|
+
if missing:
|
|
345
|
+
raise ValueError(tr("create.missing_flags", flags=", ".join(missing)))
|
|
346
|
+
return build_attribute(
|
|
347
|
+
attribute_category=args.category,
|
|
348
|
+
attribute_name=args.name,
|
|
349
|
+
display_name_ja=args.display_ja,
|
|
350
|
+
display_name_en=args.display_en,
|
|
351
|
+
weight_dimension=args.weight,
|
|
352
|
+
description_ja=args.desc_ja,
|
|
353
|
+
description_en=args.desc_en,
|
|
354
|
+
examples=args.examples,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _interactive_create() -> dict:
|
|
359
|
+
print(tr("create.wizard_header"))
|
|
360
|
+
category = _prompt_choice(tr("create.label_category"), list(CATEGORY_ORDER))
|
|
361
|
+
name = input(tr("create.ask_name")).strip()
|
|
362
|
+
display_ja = input(tr("create.ask_display_ja")).strip()
|
|
363
|
+
display_en = input(tr("create.ask_display_en")).strip()
|
|
364
|
+
weight = _prompt_choice("weight_dimension", ["none", "mild", "moderate", "strong"])
|
|
365
|
+
desc_ja = input(tr("create.ask_desc_ja")).strip()
|
|
366
|
+
desc_en = input(tr("create.ask_desc_en")).strip()
|
|
367
|
+
print(tr("create.ask_examples"))
|
|
368
|
+
examples: list[str] = []
|
|
369
|
+
while True:
|
|
370
|
+
line = input(tr("create.ask_example")).strip()
|
|
371
|
+
if not line:
|
|
372
|
+
break
|
|
373
|
+
examples.append(line)
|
|
374
|
+
return build_attribute(
|
|
375
|
+
attribute_category=category,
|
|
376
|
+
attribute_name=name,
|
|
377
|
+
display_name_ja=display_ja,
|
|
378
|
+
display_name_en=display_en,
|
|
379
|
+
weight_dimension=weight,
|
|
380
|
+
description_ja=desc_ja,
|
|
381
|
+
description_en=desc_en,
|
|
382
|
+
examples=examples or ["(example)"],
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _prompt_choice(label: str, choices: list[str]) -> str:
|
|
387
|
+
while True:
|
|
388
|
+
raw = input(tr("prompt.choice", label=label, choices=choices)).strip()
|
|
389
|
+
if raw in choices:
|
|
390
|
+
return raw
|
|
391
|
+
print(tr("prompt.invalid_choice", choices=choices))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _cmd_measure(args: argparse.Namespace) -> int:
|
|
395
|
+
if not args.input and args.text is None:
|
|
396
|
+
raise ValueError(tr("measure.need_input"))
|
|
397
|
+
|
|
398
|
+
if args.input:
|
|
399
|
+
with open(args.input, encoding="utf-8") as f:
|
|
400
|
+
text = f.read()
|
|
401
|
+
else:
|
|
402
|
+
text = args.text or ""
|
|
403
|
+
|
|
404
|
+
names = [_normalize_name(n) for n in args.names]
|
|
405
|
+
attrs = [load_attribute(n) for n in names]
|
|
406
|
+
|
|
407
|
+
reason = intensity_skip_reason(text, attrs)
|
|
408
|
+
if reason == "no_speech":
|
|
409
|
+
print(tr("measure.no_speech"))
|
|
410
|
+
return 0
|
|
411
|
+
if reason == "unsupported_lang":
|
|
412
|
+
print(tr("measure.unsupported_lang", lang=content_language(attrs)))
|
|
413
|
+
return 0
|
|
414
|
+
if reason == "lang_mismatch":
|
|
415
|
+
print(tr("measure.lang_mismatch"))
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
report = verify_intensity(text, attrs, args.weight)
|
|
419
|
+
if report is None:
|
|
420
|
+
print(tr("measure.no_speech"))
|
|
421
|
+
return 0
|
|
422
|
+
|
|
423
|
+
print(format_report(report, args.weight))
|
|
424
|
+
if report.status == "under":
|
|
425
|
+
lo, hi = report.band
|
|
426
|
+
print(
|
|
427
|
+
tr("measure.under", lo=lo, hi=hi, actual=f"{report.score:.0f}"),
|
|
428
|
+
file=sys.stderr,
|
|
429
|
+
)
|
|
430
|
+
return 0
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
sys.exit(main())
|
hersona/core/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""hersona core — 属性ロジックの本体。"""
|
|
2
|
+
|
|
3
|
+
from hersona.core.attach import (
|
|
4
|
+
BlendResult,
|
|
5
|
+
available_attributes,
|
|
6
|
+
load_attribute,
|
|
7
|
+
render_blend,
|
|
8
|
+
)
|
|
9
|
+
from hersona.core.authoring import (
|
|
10
|
+
AuthoringError,
|
|
11
|
+
ShareGuardError,
|
|
12
|
+
ValidationGateError,
|
|
13
|
+
assert_shareable,
|
|
14
|
+
build_attribute,
|
|
15
|
+
find_proper_noun_risks,
|
|
16
|
+
list_user_attributes,
|
|
17
|
+
override_attribute,
|
|
18
|
+
save_attribute,
|
|
19
|
+
user_attributes_root,
|
|
20
|
+
validate_attribute,
|
|
21
|
+
)
|
|
22
|
+
from hersona.core.compatibility import (
|
|
23
|
+
Attribute,
|
|
24
|
+
CompatibilityMatrix,
|
|
25
|
+
Relation,
|
|
26
|
+
load_matrix,
|
|
27
|
+
)
|
|
28
|
+
from hersona.core.intensity import (
|
|
29
|
+
IntensityReport,
|
|
30
|
+
expected_band,
|
|
31
|
+
format_report,
|
|
32
|
+
measure_intensity,
|
|
33
|
+
)
|
|
34
|
+
from hersona.core.intensity import (
|
|
35
|
+
verify as verify_intensity,
|
|
36
|
+
)
|
|
37
|
+
from hersona.core.recommend import (
|
|
38
|
+
DEFAULT_QUIZ,
|
|
39
|
+
DEFAULT_QUIZ_PATH,
|
|
40
|
+
RECOMMEND_THRESHOLDS,
|
|
41
|
+
QuizOption,
|
|
42
|
+
QuizQuestion,
|
|
43
|
+
Recommendation,
|
|
44
|
+
WeightMagnitude,
|
|
45
|
+
load_quiz,
|
|
46
|
+
recommend,
|
|
47
|
+
score_answers,
|
|
48
|
+
)
|
|
49
|
+
from hersona.core.weight import (
|
|
50
|
+
WEIGHT_GUIDANCE,
|
|
51
|
+
WeightLevel,
|
|
52
|
+
catchphrase_subset,
|
|
53
|
+
coerce_level,
|
|
54
|
+
suggest_weight,
|
|
55
|
+
weight_for_score,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# compatibility
|
|
60
|
+
"Attribute",
|
|
61
|
+
"CompatibilityMatrix",
|
|
62
|
+
"Relation",
|
|
63
|
+
"load_matrix",
|
|
64
|
+
# recommend
|
|
65
|
+
"QuizOption",
|
|
66
|
+
"QuizQuestion",
|
|
67
|
+
"Recommendation",
|
|
68
|
+
"DEFAULT_QUIZ",
|
|
69
|
+
"DEFAULT_QUIZ_PATH",
|
|
70
|
+
"RECOMMEND_THRESHOLDS",
|
|
71
|
+
"WeightMagnitude",
|
|
72
|
+
"load_quiz",
|
|
73
|
+
"score_answers",
|
|
74
|
+
"recommend",
|
|
75
|
+
# attach / blend
|
|
76
|
+
"BlendResult",
|
|
77
|
+
"available_attributes",
|
|
78
|
+
"load_attribute",
|
|
79
|
+
"render_blend",
|
|
80
|
+
# weight
|
|
81
|
+
"WeightLevel",
|
|
82
|
+
"WEIGHT_GUIDANCE",
|
|
83
|
+
"catchphrase_subset",
|
|
84
|
+
"coerce_level",
|
|
85
|
+
"suggest_weight",
|
|
86
|
+
"weight_for_score",
|
|
87
|
+
# intensity
|
|
88
|
+
"IntensityReport",
|
|
89
|
+
"expected_band",
|
|
90
|
+
"format_report",
|
|
91
|
+
"measure_intensity",
|
|
92
|
+
"verify_intensity",
|
|
93
|
+
# authoring
|
|
94
|
+
"AuthoringError",
|
|
95
|
+
"ValidationGateError",
|
|
96
|
+
"ShareGuardError",
|
|
97
|
+
"build_attribute",
|
|
98
|
+
"override_attribute",
|
|
99
|
+
"validate_attribute",
|
|
100
|
+
"save_attribute",
|
|
101
|
+
"list_user_attributes",
|
|
102
|
+
"user_attributes_root",
|
|
103
|
+
"find_proper_noun_risks",
|
|
104
|
+
"assert_shareable",
|
|
105
|
+
]
|