-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathtext_processor.py
More file actions
766 lines (600 loc) · 23.5 KB
/
Copy pathtext_processor.py
File metadata and controls
766 lines (600 loc) · 23.5 KB
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
"""
文本处理模块 - 负责假名标注、拼音标注、双语裁剪和显示格式化
"""
from typing import Optional
import config
from shared.vrchat_text_limits import trim_text_prefix_to_limit
# ============ 双语输出常量 ============
DUAL_OUTPUT_SEPARATOR = "\n"
# 双语裁剪时:中日韩权重=1,其他语言权重=2。
COMPACT_SCRIPT_LANGUAGE_BASES = {'zh', 'ja', 'ko'}
COMPACT_SCRIPT_BUDGET_WEIGHT = 1
ALPHABETIC_SCRIPT_BUDGET_WEIGHT = 2
TEXT_FANCY_STYLE_OPTIONS = (
('none', 'No Effect'),
('smallCaps', 'SMALLCAPS'),
('curly', 'curly'),
('magic', 'magic'),
)
TEXT_FANCY_STYLE_VALUES = frozenset(value for value, _ in TEXT_FANCY_STYLE_OPTIONS)
RTL_RLI = "\u2067"
RTL_PDI = "\u2069"
RTL_OSC_LINE_MAX_CHARS = 30
RTL_OSC_MAX_LINE_BREAKS = 3
RTL_OSC_MAX_LINES = RTL_OSC_MAX_LINE_BREAKS + 1
# Backward-compatible names for the existing Arabic OSC path/tests.
ARABIC_RLI = RTL_RLI
ARABIC_PDI = RTL_PDI
ARABIC_OSC_LINE_MAX_CHARS = RTL_OSC_LINE_MAX_CHARS
try:
import fancify_text as _fancify_text
except ImportError:
_fancify_text = None
try:
import arabic_reshaper as _arabic_reshaper
except Exception:
_arabic_reshaper = None
try:
from bidi.algorithm import get_display as _bidi_get_display
except Exception:
_bidi_get_display = None
_TEXT_POST_PROCESSING_DEGRADATION_WARNINGS = set()
def _warn_text_post_processing_degraded_once(key: str, message: str) -> None:
if key in _TEXT_POST_PROCESSING_DEGRADATION_WARNINGS:
return
_TEXT_POST_PROCESSING_DEGRADATION_WARNINGS.add(key)
print(f"[TextPost] {message}")
def _get_dual_output_limits() -> tuple[Optional[int], Optional[int], Optional[int]]:
total_max_chars = config.get_effective_osc_text_max_length()
if total_max_chars is None:
return None, None, None
body_budget = max(0, total_max_chars - len(DUAL_OUTPUT_SEPARATOR))
max_chars_per_result = body_budget // 2
return total_max_chars, body_budget, max_chars_per_result
# ============ 语言代码工具函数 ============
def normalize_optional_language_code(language: Optional[str]) -> Optional[str]:
if language is None:
return None
normalized = str(language).strip()
return normalized or None
def normalize_lang_code(lang):
"""标准化语言代码。
中文区分简体 (zh-hans) 与繁体 (zh-hant),以便在目标语为 zh-CN / zh-TW 等时
仍能触发简繁互译;检测器返回的泛化 ``zh`` 视为简体(与 zh-CN 一致)。
"""
if not lang:
return 'auto'
lang_norm = str(lang).strip().lower().replace('_', '-')
if lang_norm == 'auto':
return 'auto'
if lang_norm in ('en', 'en-us', 'en-gb', 'en-au', 'en-ca', 'en-nz', 'en-ie'):
return 'en'
# 繁体中文(含港澳地区常用码)
if lang_norm in ('zh-tw', 'zh-hant', 'zh-hk', 'zh-mo'):
return 'zh-hant'
# 简体中文 + 无简繁信息的「zh」(检测器/ASR 常见返回值)
if lang_norm in (
'zh',
'zh-cn',
'zh-hans',
'zh-sg',
'cmn',
'wuu',
'yue',
):
return 'zh-hans'
return lang_norm
def language_code_for_osc_tag(lang: Optional[str]) -> str:
"""OSC 等场景的语言对标记:只保留主语言码,不区分地区/简繁/script 变体。"""
if lang is None:
return 'auto'
norm = str(lang).strip().lower().replace('_', '-')
if not norm:
return 'auto'
if norm == 'auto':
return 'auto'
if norm in (
'zh',
'zh-cn',
'zh-hans',
'zh-tw',
'zh-hant',
'zh-hk',
'zh-mo',
'zh-sg',
'cmn',
'wuu',
'yue',
):
return 'zh'
return norm.split('-', 1)[0]
def has_secondary_translation_target() -> bool:
return normalize_optional_language_code(
getattr(config, 'SECONDARY_TARGET_LANGUAGE', None)
) is not None
def resolve_output_target_language(
source_language: str,
requested_target_language: Optional[str],
) -> Optional[str]:
target_language = normalize_optional_language_code(requested_target_language)
if target_language is None:
return None
fallback_language = normalize_optional_language_code(
getattr(config, 'FALLBACK_LANGUAGE', None)
)
if fallback_language and normalize_lang_code(source_language) == normalize_lang_code(target_language):
return fallback_language
return target_language
# ============ 输出格式化 ============
def _sanitize_output_line(text: str) -> str:
if not text:
return ""
normalized = text.replace('\r\n', '\n').replace('\r', '\n')
return " ".join(part.strip() for part in normalized.split('\n') if part.strip())
def sanitize_text_fancy_style(style: Optional[str]) -> str:
if style is None:
return 'none'
normalized = str(style).strip()
if normalized in TEXT_FANCY_STYLE_VALUES:
return normalized
return 'none'
def apply_text_fancy_style_if_needed(text: str) -> str:
sanitized = _sanitize_output_line(text)
if not sanitized:
return ""
style = sanitize_text_fancy_style(getattr(config, 'TEXT_FANCY_STYLE', 'none'))
if style == 'none':
return sanitized
if _fancify_text is None:
_warn_text_post_processing_degraded_once(
'fancify_text_missing',
'文本风格后处理降级:fancify-text 不可用,输出原文。',
)
return sanitized
style_fn = getattr(_fancify_text, style, None)
if not callable(style_fn):
return sanitized
try:
return str(style_fn(sanitized))
except Exception:
return sanitized
def _contains_arabic_reshapable_text(text: str) -> bool:
"""Detect Arabic-script source characters, excluding presentation-form output."""
return any(
'\u0600' <= ch <= '\u06ff'
or '\u0750' <= ch <= '\u077f'
or '\u0870' <= ch <= '\u089f'
or '\u08a0' <= ch <= '\u08ff'
for ch in text
)
def _contains_hebrew_text(text: str) -> bool:
return any(
'\u0590' <= ch <= '\u05ff'
or '\ufb1d' <= ch <= '\ufb4f'
for ch in text
)
def _contains_rtl_reorderable_text(text: str) -> bool:
return _contains_arabic_reshapable_text(text) or _contains_hebrew_text(text)
def is_rtl_isolate_wrapped(text: str) -> bool:
return bool(text) and text.startswith(RTL_RLI) and text.endswith(RTL_PDI)
def is_arabic_rtl_isolate_wrapped(text: str) -> bool:
return is_rtl_isolate_wrapped(text)
def wrap_rtl_isolate(text: str) -> str:
if not text or is_rtl_isolate_wrapped(text):
return text
return f"{RTL_RLI}{text}{RTL_PDI}"
def wrap_arabic_rtl_isolate(text: str) -> str:
return wrap_rtl_isolate(text)
def _wrap_text_at_word_boundaries(text: str, max_chars: int) -> list[str]:
if not text or max_chars <= 0:
return []
lines: list[str] = []
normalized = text.replace('\r\n', '\n').replace('\r', '\n')
for source_line in normalized.split('\n'):
words = source_line.split()
current = ""
for word in words:
if len(word) > max_chars:
if current:
lines.append(current)
current = ""
for start in range(0, len(word), max_chars):
lines.append(word[start:start + max_chars])
continue
if not current:
current = word
elif len(current) + 1 + len(word) <= max_chars:
current = f"{current} {word}"
else:
lines.append(current)
current = word
if current:
lines.append(current)
return lines
def _limit_line_at_word_boundary(text: str, max_chars: int) -> str:
if max_chars <= 0:
return ""
if len(text) <= max_chars:
return text
words = text.split()
current = ""
for word in words:
if len(word) > max_chars:
return current or word[:max_chars]
if not current:
current = word
elif len(current) + 1 + len(word) <= max_chars:
current = f"{current} {word}"
else:
break
return current
def _line_needs_rtl_processing(line: str) -> bool:
return _contains_rtl_reorderable_text(line)
def _line_needs_arabic_processing(line: str) -> bool:
return _line_needs_rtl_processing(line)
def _process_rtl_line(line: str) -> str:
if not _line_needs_rtl_processing(line):
return line
display_source = line
if _contains_arabic_reshapable_text(line):
if _arabic_reshaper is None:
_warn_text_post_processing_degraded_once(
'arabic_reshaper_missing',
'阿拉伯文显示重排降级:arabic-reshaper 不可用,仅添加方向隔离控制符。',
)
return wrap_rtl_isolate(line)
try:
display_source = str(_arabic_reshaper.reshape(line))
except Exception as exc:
_warn_text_post_processing_degraded_once(
'arabic_reshaper_failed',
f'阿拉伯文显示重排降级:处理失败({exc!r}),仅添加方向隔离控制符。',
)
return wrap_rtl_isolate(line)
if _bidi_get_display is None:
_warn_text_post_processing_degraded_once(
'python_bidi_missing',
'RTL 显示重排降级:python-bidi 不可用,混排英文/数字可能显示异常。',
)
return wrap_rtl_isolate(display_source)
try:
# Run bidi after manual line wrapping so mixed LTR/RTL text is fixed
# without letting renderer auto-wrap split punctuation across lines.
return wrap_rtl_isolate(str(_bidi_get_display(display_source)))
except Exception as exc:
_warn_text_post_processing_degraded_once(
'rtl_bidi_failed',
f'RTL 显示重排降级:bidi 处理失败({exc!r}),仅添加方向隔离控制符。',
)
return wrap_rtl_isolate(display_source)
def _process_arabic_line(line: str) -> str:
return _process_rtl_line(line)
def _rtl_processed_line_for_budget(line: str, inner_budget: int) -> str:
logical_line = _limit_line_at_word_boundary(line, inner_budget)
while logical_line:
processed_line = _process_rtl_line(logical_line)
line_overhead = (
len(RTL_RLI) + len(RTL_PDI)
if is_rtl_isolate_wrapped(processed_line)
else 0
)
if len(processed_line) <= inner_budget + line_overhead:
return processed_line
logical_line = logical_line[:-1].rstrip()
return ""
def _arabic_processed_line_for_budget(line: str, inner_budget: int) -> str:
return _rtl_processed_line_for_budget(line, inner_budget)
def _build_rtl_reordered_lines(
text: str,
max_chars: Optional[int] = None,
) -> str:
logical_lines = _wrap_text_at_word_boundaries(text, RTL_OSC_LINE_MAX_CHARS)
if RTL_OSC_MAX_LINES > 0:
logical_lines = logical_lines[:RTL_OSC_MAX_LINES]
if not logical_lines:
return ""
processed_lines: list[str] = []
current_length = 0
for logical_line in logical_lines:
processed_line = _process_rtl_line(logical_line)
separator_length = 1 if processed_lines else 0
if max_chars is None:
processed_lines.append(processed_line)
continue
if current_length + separator_length + len(processed_line) <= max_chars:
processed_lines.append(processed_line)
current_length += separator_length + len(processed_line)
continue
remaining = max_chars - current_length - separator_length
line_overhead = (
len(RTL_RLI) + len(RTL_PDI)
if _line_needs_rtl_processing(logical_line)
else 0
)
inner_budget = min(RTL_OSC_LINE_MAX_CHARS, remaining - line_overhead)
if inner_budget > 0:
shortened_line = _rtl_processed_line_for_budget(logical_line, inner_budget)
if shortened_line:
processed_lines.append(shortened_line)
break
return "\n".join(processed_lines)
def _build_arabic_reshaped_lines(
text: str,
max_chars: Optional[int] = None,
) -> str:
return _build_rtl_reordered_lines(text, max_chars=max_chars)
def apply_arabic_reshaper_if_needed(
text: str,
language: Optional[str] = None,
max_chars: Optional[int] = None,
) -> str:
if not text or not getattr(config, 'ENABLE_ARABIC_RESHAPER', True):
return text
if is_rtl_isolate_wrapped(text):
return text
if not _contains_rtl_reorderable_text(text):
return text
return _build_rtl_reordered_lines(text, max_chars=max_chars)
def remove_trailing_sentence_period_if_needed(text: str) -> str:
"""Optionally remove a single trailing sentence-final period."""
sanitized = _sanitize_output_line(text)
if not sanitized or not getattr(config, 'REMOVE_TRAILING_PERIOD', False):
return sanitized
trimmed = sanitized.rstrip()
if trimmed.endswith(("。", ".", ".")):
return trimmed[:-1].rstrip()
return trimmed
def apply_basic_text_post_processing(text: str) -> str:
return apply_text_fancy_style_if_needed(
remove_trailing_sentence_period_if_needed(text)
)
def _normalize_language_base(language: Optional[str]) -> str:
if language is None:
return ""
normalized = str(language).strip().lower().replace('_', '-')
if not normalized:
return ""
return normalized.split('-', 1)[0]
def _is_compact_script_language(language: Optional[str]) -> bool:
return _normalize_language_base(language) in COMPACT_SCRIPT_LANGUAGE_BASES
def _get_language_budget_weight(language: Optional[str]) -> float:
if _is_compact_script_language(language):
return COMPACT_SCRIPT_BUDGET_WEIGHT
return ALPHABETIC_SCRIPT_BUDGET_WEIGHT
def _allocate_dual_output_budgets(
primary_language: Optional[str],
secondary_language: Optional[str],
total_chars: Optional[int] = None,
) -> tuple[int, int]:
if total_chars is None:
_, total_chars, _ = _get_dual_output_limits()
if total_chars is None:
return 0, 0
if total_chars <= 0:
return 0, 0
primary_weight = _get_language_budget_weight(primary_language)
secondary_weight = _get_language_budget_weight(secondary_language)
total_weight = primary_weight + secondary_weight
if total_chars == 1:
return 1, 0
if total_weight <= 0:
primary_budget = total_chars // 2
else:
primary_budget = int(round(total_chars * (primary_weight / total_weight)))
primary_budget = max(1, min(total_chars - 1, primary_budget))
secondary_budget = total_chars - primary_budget
return primary_budget, secondary_budget
def limit_dual_output_text(
text: str,
max_chars: Optional[int] = None,
) -> str:
sanitized = _sanitize_output_line(text)
if max_chars is None:
_, _, max_chars = _get_dual_output_limits()
if max_chars is None:
return sanitized
if len(sanitized) <= max_chars:
return sanitized
return trim_text_prefix_to_limit(sanitized, max_chars)
def build_dual_output_display(
primary_text: str,
secondary_text: Optional[str],
primary_language: Optional[str] = None,
secondary_language: Optional[str] = None,
) -> str:
total_max_chars, body_budget, _ = _get_dual_output_limits()
if secondary_text is None:
return limit_dual_output_text(primary_text)
primary_sanitized = _sanitize_output_line(primary_text)
secondary_sanitized = _sanitize_output_line(secondary_text)
full_text = DUAL_OUTPUT_SEPARATOR.join([primary_sanitized, secondary_sanitized])
if total_max_chars is None or body_budget is None:
return full_text
# 能完整装下时不做任何裁剪。
if len(full_text) <= total_max_chars:
return full_text
primary_budget, secondary_budget = _allocate_dual_output_budgets(
primary_language,
secondary_language,
total_chars=body_budget,
)
clipped_primary = limit_dual_output_text(primary_sanitized, max_chars=primary_budget)
clipped_secondary = limit_dual_output_text(secondary_sanitized, max_chars=secondary_budget)
return DUAL_OUTPUT_SEPARATOR.join([clipped_primary, clipped_secondary])
# 形如 [en→zh] 译文 (原文) 的带标签译文行的装饰符号
TAGGED_TRANSLATION_SOURCE_PREFIX = " ("
TAGGED_TRANSLATION_SOURCE_SUFFIX = ")"
def build_tagged_translation_display(
source_tag: str,
target_tag: str,
translated_text: str,
source_text: Optional[str] = None,
max_chars: Optional[int] = None,
) -> str:
"""拼接形如 ``[src→tgt] 译文 (原文)`` 的单行 OSC 文本。
语言标签前缀 ``[src→tgt] ``、括住原文的 `` (`` / ``)`` 等装饰符号的长度都会
计入字符预算。超长时按「丢弃最前面的旧文本」的统一规则裁剪,且:
* 优先整段丢弃括号中的原文(不会残留半个括号);
* 若译文本身仍然超长,则保留语言标签前缀,仅对译文按统一规则丢弃前半部分。
这样最终长度保证不超过 ``max_chars``,且不会把语言标签或括号裁成残缺的装饰。
"""
if max_chars is None:
max_chars = config.get_effective_osc_text_max_length()
prefix = f"[{source_tag}→{target_tag}] "
translated = translated_text if translated_text is not None else ""
source = source_text or ""
suffix = (
f"{TAGGED_TRANSLATION_SOURCE_PREFIX}{source}{TAGGED_TRANSLATION_SOURCE_SUFFIX}"
if source
else ""
)
full = f"{prefix}{translated}{suffix}"
if max_chars is None or len(full) <= max_chars:
return full
# 第一步:丢弃括号中的原文(连同 " (" / ")" 装饰一起去掉,不残留半个括号)。
without_source = f"{prefix}{translated}"
if len(without_source) <= max_chars:
return without_source
# 第二步:译文仍然超长——保留语言标签前缀,对译文按统一规则丢弃前半部分。
body_budget = max_chars - len(prefix)
if body_budget <= 0:
# 极端情况(连标签前缀都放不下):对整体按统一规则丢弃前半部分兜底。
return trim_text_prefix_to_limit(without_source, max_chars)
trimmed = trim_text_prefix_to_limit(translated, body_budget)
return f"{prefix}{trimmed}"
def build_streaming_output_line(text: str) -> str:
formatted_text = apply_basic_text_post_processing(text)
if formatted_text.endswith("……"):
return formatted_text
if formatted_text:
return f"{formatted_text}……"
return "……"
# ============ 可选的日语假名标注支持 ============
try:
from pykakasi import kakasi as _kakasi_factory
_kakasi = _kakasi_factory()
_kakasi.setMode("J", "H") # Kanji -> Hiragana
_kakasi.setMode("K", "H") # Katakana -> Hiragana
_kakasi.setMode("H", "H") # Hiragana stays Hiragana
except Exception:
_kakasi = None
# ============ 可选的中文拼音标注支持 ============
try:
from pypinyin import pinyin, Style
_pypinyin_available = True
except Exception:
_pypinyin_available = False
def _contains_kanji(text: str) -> bool:
"""Check if the text contains any CJK ideographs."""
return any('\u4e00' <= ch <= '\u9fff' for ch in text)
def _contains_chinese(text: str) -> bool:
"""Check if the text contains Chinese characters."""
return any('\u4e00' <= ch <= '\u9fff' for ch in text)
def add_furigana(text: str) -> str:
"""Add hiragana readings to Japanese text with kanji."""
if not text:
return text
if _kakasi is None:
return text
try:
tokens = _kakasi.convert(text)
parts = []
for token in tokens:
orig = token.get('orig', '')
hira = token.get('hira') or token.get('kana')
if orig and _contains_kanji(orig) and hira and hira != orig:
parts.append(f"{orig}({hira})")
else:
parts.append(orig)
return "".join(parts)
except Exception as exc:
_warn_text_post_processing_degraded_once(
'pykakasi_failed',
f'日语假名后处理降级:处理失败({exc!r}),输出原文。',
)
return text
def add_pinyin(text: str) -> str:
"""Add pinyin with tones to Chinese text, grouped by words.
Uses jieba for word segmentation. Output format: 大家dà'jiā晚上好wǎn'shàng'hǎo
"""
if not text:
return text
if not _pypinyin_available:
_warn_text_post_processing_degraded_once(
'pypinyin_missing',
'中文拼音后处理降级:pypinyin 不可用,输出原文。',
)
return text
if not _contains_chinese(text):
return text
try:
import jieba
full_pinyin = pinyin(text, style=Style.TONE)
char_to_pinyin = {}
for i, char in enumerate(text):
if i < len(full_pinyin):
py = full_pinyin[i][0]
if _contains_chinese(char) and py != char:
char_to_pinyin[i] = py
words = list(jieba.cut(text))
result_parts = []
char_index = 0
for word in words:
if _contains_chinese(word):
word_pinyins = []
for char in word:
if char_index in char_to_pinyin:
word_pinyins.append(char_to_pinyin[char_index])
char_index += 1
if word_pinyins:
py_str = "'".join(word_pinyins)
result_parts.append(f"{word}{py_str}")
else:
result_parts.append(word)
else:
result_parts.append(word)
char_index += len(word)
return "".join(result_parts)
except ImportError as exc:
_warn_text_post_processing_degraded_once(
'jieba_missing',
f'中文拼音后处理降级:jieba 不可用({exc!r}),输出原文。',
)
return text
except Exception as exc:
_warn_text_post_processing_degraded_once(
'pinyin_failed',
f'中文拼音后处理降级:处理失败({exc!r}),输出原文。',
)
return text
def add_furigana_if_needed(text: str, language: str) -> str:
"""Add furigana to text if it's Japanese and furigana is enabled."""
if not text or not getattr(config, 'ENABLE_JA_FURIGANA', False):
return text
lang = (language or '').lower()
if not lang.startswith('ja'):
return text
if _kakasi is None:
_warn_text_post_processing_degraded_once(
'pykakasi_missing',
'日语假名后处理降级:pykakasi 不可用,输出原文。',
)
return text
return add_furigana(text)
def add_pinyin_if_needed(text: str, language: str) -> str:
"""Add pinyin to text if it's Chinese and pinyin is enabled."""
if not text or not getattr(config, 'ENABLE_ZH_PINYIN', False):
return text
lang = (language or '').lower()
if not lang.startswith('zh'):
return text
return add_pinyin(text)
def get_display_text(text: str, language: Optional[str] = None) -> str:
display_text = text
if language is not None:
display_text = add_furigana_if_needed(display_text, language)
display_text = add_pinyin_if_needed(display_text, language)
return apply_basic_text_post_processing(display_text)
def get_display_translation_text(translated_text: str, target_language: str) -> str:
"""为翻译结果添加假名/拼音标注。"""
return get_display_text(translated_text, target_language)