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.
Files changed (89) hide show
  1. hersona/__init__.py +10 -0
  2. hersona/cli/__init__.py +9 -0
  3. hersona/cli/__main__.py +7 -0
  4. hersona/cli/app.py +434 -0
  5. hersona/core/__init__.py +105 -0
  6. hersona/core/attach.py +271 -0
  7. hersona/core/authoring.py +317 -0
  8. hersona/core/compatibility.py +250 -0
  9. hersona/core/constants.py +18 -0
  10. hersona/core/i18n.py +171 -0
  11. hersona/core/intensity.py +296 -0
  12. hersona/core/paths.py +32 -0
  13. hersona/core/recommend.py +505 -0
  14. hersona/core/weight.py +144 -0
  15. hersona/data/attributes/archetype/childhood_friend.yaml +25 -0
  16. hersona/data/attributes/archetype/gamer_otaku.yaml +25 -0
  17. hersona/data/attributes/archetype/heroine.yaml +25 -0
  18. hersona/data/attributes/archetype/hikikomori.yaml +61 -0
  19. hersona/data/attributes/archetype/idol.yaml +61 -0
  20. hersona/data/attributes/archetype/mentor.yaml +25 -0
  21. hersona/data/attributes/archetype/rival.yaml +25 -0
  22. hersona/data/attributes/archetype/robot_android.yaml +27 -0
  23. hersona/data/attributes/archetype/shrine_maiden.yaml +27 -0
  24. hersona/data/attributes/hobby/cooking.yaml +54 -0
  25. hersona/data/attributes/hobby/gamer.yaml +54 -0
  26. hersona/data/attributes/hobby/music.yaml +54 -0
  27. hersona/data/attributes/hobby/reading.yaml +55 -0
  28. hersona/data/attributes/hobby/sports.yaml +55 -0
  29. hersona/data/attributes/personality/airhead.yaml +129 -0
  30. hersona/data/attributes/personality/chuunibyou.yaml +129 -0
  31. hersona/data/attributes/personality/dandere.yaml +126 -0
  32. hersona/data/attributes/personality/genki.yaml +127 -0
  33. hersona/data/attributes/personality/hot_blooded.yaml +130 -0
  34. hersona/data/attributes/personality/intellectual.yaml +129 -0
  35. hersona/data/attributes/personality/klutz.yaml +128 -0
  36. hersona/data/attributes/personality/kuudere.yaml +127 -0
  37. hersona/data/attributes/personality/mysterious.yaml +130 -0
  38. hersona/data/attributes/personality/narcissist.yaml +129 -0
  39. hersona/data/attributes/personality/optimist.yaml +127 -0
  40. hersona/data/attributes/personality/pessimist.yaml +126 -0
  41. hersona/data/attributes/personality/playful.yaml +127 -0
  42. hersona/data/attributes/personality/pragmatist.yaml +130 -0
  43. hersona/data/attributes/personality/protective.yaml +128 -0
  44. hersona/data/attributes/personality/serious.yaml +126 -0
  45. hersona/data/attributes/personality/stoic.yaml +126 -0
  46. hersona/data/attributes/personality/switch.yaml +127 -0
  47. hersona/data/attributes/personality/tsundere.yaml +124 -0
  48. hersona/data/attributes/personality/yandere.yaml +127 -0
  49. hersona/data/attributes/speech/archaic.yaml +29 -0
  50. hersona/data/attributes/speech/blunt.yaml +48 -0
  51. hersona/data/attributes/speech/blunt_en.yaml +36 -0
  52. hersona/data/attributes/speech/boku_girl.yaml +29 -0
  53. hersona/data/attributes/speech/british_en.yaml +41 -0
  54. hersona/data/attributes/speech/casual_en.yaml +38 -0
  55. hersona/data/attributes/speech/formal_en.yaml +39 -0
  56. hersona/data/attributes/speech/gyaru.yaml +49 -0
  57. hersona/data/attributes/speech/kansai_ben.yaml +29 -0
  58. hersona/data/attributes/speech/keigo.yaml +30 -0
  59. hersona/data/attributes/speech/kyoto_ben.yaml +67 -0
  60. hersona/data/attributes/speech/mischievous.yaml +47 -0
  61. hersona/data/attributes/speech/mixed_dialect.yaml +47 -0
  62. hersona/data/attributes/speech/onee_kotoba.yaml +29 -0
  63. hersona/data/attributes/speech/ore_boy.yaml +30 -0
  64. hersona/data/attributes/speech/princess_speech.yaml +48 -0
  65. hersona/data/attributes/speech/seductive.yaml +50 -0
  66. hersona/data/attributes/speech/soft.yaml +48 -0
  67. hersona/data/attributes/speech/southern_us_en.yaml +41 -0
  68. hersona/data/attributes/speech/stutter.yaml +43 -0
  69. hersona/data/attributes/speech/theatrical.yaml +47 -0
  70. hersona/data/attributes/speech/third_person.yaml +29 -0
  71. hersona/data/attributes/speech/tomboy.yaml +49 -0
  72. hersona/data/attributes/speech/washi.yaml +64 -0
  73. hersona/data/attributes/speech/whispery.yaml +29 -0
  74. hersona/data/attributes/visual/animal_ears.yaml +55 -0
  75. hersona/data/attributes/visual/glamorous.yaml +54 -0
  76. hersona/data/attributes/visual/glasses.yaml +55 -0
  77. hersona/data/attributes/visual/petite.yaml +54 -0
  78. hersona/data/attributes/visual/silver_hair.yaml +56 -0
  79. hersona/data/quiz/recommend_quiz.en.yaml +400 -0
  80. hersona/data/quiz/recommend_quiz.yaml +257 -0
  81. hersona/data/schema/attribute.schema.json +225 -0
  82. hersona/locales/en.yaml +116 -0
  83. hersona/locales/ja.yaml +114 -0
  84. hersona-0.0.1.dist-info/METADATA +214 -0
  85. hersona-0.0.1.dist-info/RECORD +89 -0
  86. hersona-0.0.1.dist-info/WHEEL +4 -0
  87. hersona-0.0.1.dist-info/entry_points.txt +2 -0
  88. hersona-0.0.1.dist-info/licenses/LICENSE +22 -0
  89. hersona-0.0.1.dist-info/licenses/LICENSE-CC0.txt +73 -0
hersona/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """hersona — 汎用属性テンプレート集のコアロジック。
2
+
3
+ ロードマップ (ROADMAP.md) の core 共有設計に基づき、attach / blend / check /
4
+ recommend / authoring のロジックを本パッケージに集約する。Hermes スキル・
5
+ CLI/TUI・将来の Web 殻はいずれも本パッケージの薄い殻となる。
6
+ """
7
+
8
+ __all__ = ["__version__"]
9
+
10
+ __version__ = "0.2.0"
@@ -0,0 +1,9 @@
1
+ """hersona CLI/TUI 殻 (ROADMAP: 対話 CLI/TUI)。
2
+
3
+ core ロジック (compatibility / authoring / recommend / attach) の薄い殻。
4
+ エントリポイントは `hersona.cli.main`。
5
+ """
6
+
7
+ from hersona.cli.app import main
8
+
9
+ __all__ = ["main"]
@@ -0,0 +1,7 @@
1
+ """`python -m hersona.cli` エントリポイント。"""
2
+ import sys
3
+
4
+ from hersona.cli.app import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
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())
@@ -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
+ ]