Files
VoiceStudio/tests/test_no_hardcoded_cjk.py
T
Palash DebnathandClaude Fable 5 2d5f2e800e feat(omnivoice): voice prompts that survive restarts + opt-in FlashInfer (~2.2x) (#1565)
* feat(omnivoice): port upstream VoiceClonePrompt persistence + FlashInfer opt-in

Upstream k2-fsa teardown ports, verified with generated voice samples:

- VoiceClonePrompt.save()/.load() (upstream format v1, weights_only-safe)
  on the vendored model, and a disk layer under the in-memory prompt LRU
  (DATA_DIR/prompt_cache, keyed by ref path+mtime+ref_text+preprocess,
  32 newest kept, OMNIVOICE_PROMPT_DISK_CACHE=0 opts out). First generation
  of a session with a known voice skips the reference re-encode and any
  auto-transcription pass — verified across two real processes (encodes=1
  then encodes=0, same voice).
- omnivoice_flashinfer.py ported (packed CFG attention, fused kernels,
  optional CUDA graphs), schedule adapted to our num_step+1 divergence.
  Opt-in via OMNIVOICE_FLASHINFER=1|graph, CUDA-only, replaces
  torch.compile for the session; missing package / apply failure / runtime
  failure all degrade with a named reason (same #278 contract as compile:
  classify → unapply → retry once, session latch). Measured 2.20x at
  batch=1 on an RTX 4090 with byte-identical text and clean ASR round-trip.
- Docs: OmniVoice guide gains instruct+reference combination semantics
  (consistent instruct stabilizes cloning, reference wins conflicts),
  inline pronunciation control (pinyin / CMU), prompt persistence, and
  corrects the 'no voice design' claim; performance.md documents both new
  env knobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: point changelog entries at the real PR number (#1565)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pr): harden FlashInfer lifecycle + prompt-cache writes per review

Bot harvest round 1 (#1565): unapply on apply-failure (half-patched model
could crash the next render); pin eager-mode FlashInfer inference to one
thread too — the attention plan and packed position ids are per-generation
module state, so interleaved _gpu_pool workers would corrupt each other;
restore the CAPTURED pre-apply attention impl (could be flash_attention_2)
instead of assuming sdpa; unique tmp name per prompt-cache write; correct
the _forward_logits layout docstring; resolve VoiceClonePrompt at test
runtime; docs — Known limits keeps only the limitation, performance.md
states the VRAM cost and scopes the fallback claim to classified kernel
failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pr): round-2 review — publish only a fully restored model, redact latch reason, tighten CPU-persistence test

Greptile: the runtime fallback now unapplies BEFORE swapping generate, so
a concurrent render keeps queuing behind the thread-affinity wrapper while
teardown mutates modules. CodeRabbit: FlashInfer failure reasons pass
through core.failure.sanitize before latching/logging (wheel paths embed
the user's home); the save-portability test now creates the tokens on CUDA
when available and asserts the persisted payload itself is CPU-resident.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pr): fail-closed latch reason when the sanitizer itself breaks

CodeQL empty-except + CodeRabbit round 3: if core.failure.sanitize raises,
the raw reason (home paths, wheel paths) was latched anyway. Now only the
exception class survives with a fixed redaction note; two regression tests
(normal redaction + sanitizer failure).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:25:29 +00:00

167 lines
7.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Hard rule: no hardcoded non-English (CJK) *user-facing* text outside the
translation layer.
UI strings must go through i18n (``frontend/src/i18n/locales/*.json`` via
``t('...')``); native language names live in ``frontend/src/i18n/index.ts``.
This guards against contributors hardcoding Chinese/Japanese/Korean display
strings in component or app code (see CLAUDE.md > Conventions).
Functional CJK is permitted and tracked in ``_ALLOWED_FILES`` below:
text-processing regexes, model/engine vocabulary & identifiers, localized
error matching, demo/eval data, and test fixtures. To add a new legitimate
functional-CJK file, extend ``_ALLOWED_FILES`` with a one-line justification.
"""
import os
import re
from pathlib import Path
import pytest
_REPO = Path(__file__).resolve().parents[1]
# CJK punctuation, kana, CJK Ext-A, CJK unified ideographs, hangul, and
# fullwidth forms. (This enforcement file lives under tests/, which is
# allowlisted, so its own range literals don't trip the check.)
_CJK = re.compile(
"[ -〿぀-ヿ㐀-䶿一-鿿가-힯＀-￯]"
)
_SKIP_DIRS = {
".git", "node_modules", "dist", "build", "__pycache__",
".venv", "venv", "target", ".specify", ".claude", ".pytest_cache",
}
_SKIP_EXT = {
".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".svg",
".woff", ".woff2", ".ttf", ".otf", ".pdf", ".mp3", ".wav",
".mp4", ".lock", ".bin", ".onnx", ".safetensors",
}
# The translation layer: locale JSON + native language names (LANGUAGES).
# Plus design-spec docs under docs/specs/, which legitimately quote functional
# CJK (test-fixture descriptions, model/engine identifiers like CosyVoice
# speaker IDs, multilingual sample text) — they are documentation, not shipped
# UI strings, so they belong on the same footing as the allowlisted docs below.
_ALLOWED_PREFIXES = ("frontend/src/i18n/", "docs/specs/")
# Functional / data / documentation files where CJK is intentional and required.
_ALLOWED_FILES = {
# Documentation & translated docs
"README.md", # native language-switcher link
"README_CN.md", # Chinese README (a translation)
"docs/data_preparation.md", # multilingual example payloads
"docs/voice-design.md", # EN/CJK attribute mapping table
"docs/engines/omnivoice.md", # pinyin pronunciation-control example (functional CJK)
"docs/superpowers/specs/2026-05-31-voice-gallery-design.md", # Chinese-dialect taxonomy reference table
"examples/README.md", # multilingual example payloads
# Text-processing (CJK punctuation inside sentence/clause-splitting regexes)
"backend/services/segmentation.py",
"backend/services/sentence_chunker.py", # streaming-TTS terminator tables (Patter port, Wave 1.4)
"backend/services/subtitle_segmenter.py",
"backend/core/http_headers.py", # docstring quotes the CJK filename that 500'd the header (#1262)
"frontend/src/components/DubSegmentRow.jsx",
"frontend/src/components/StoriesEditor.jsx",
"frontend/src/utils/voiceInstruct.js",
"omnivoice/utils/text.py",
# Model / engine vocabulary & identifiers (the model/engine requires these)
"backend/services/tts_backend.py", # CosyVoice speaker IDs
"backend/core/personalities.py", # Chinese-dialect showcase preset
"backend/core/archetypes.py", # Chinese-dialect + JA/KO multilingual preview sample text
"backend/core/describe_voice.py", # pinyin → Chinese-dialect token mapping (model vocabulary, #317)
"frontend/src/utils/constants.js", # Chinese-dialect picker names
"omnivoice/models/omnivoice.py", # instruct-mode vocabulary
"omnivoice/utils/duration.py",
"omnivoice/utils/voice_design.py", # EN/CJK attribute maps
"backend/migrations/versions/0007_rebuild_poisoned_design_instruct.py", # frozen CJK dialect-tag snapshot for the instruct heal (#564)
# Localized error matching (classify OS errors reported in Chinese, #72)
"frontend/src/utils/errorDocsMap.ts",
# WER evaluation data
"omnivoice/eval/wer/fleurs.py",
"omnivoice/eval/wer/punctuations.lst",
# CLI demo (bilingual demo labels; not the shipped app)
"omnivoice/cli/demo.py",
# Demo-audio generation scripts (multilingual TTS sample text)
"scripts/build_demos.sh",
"scripts/build_dub_demo.sh",
"scripts/dub_demo_scripts.json", # the five dub paragraphs + their native language labels
# Rendered dubbing-demo bundle: the demo IS a dub into Chinese and
# Japanese, so its subtitles and manifest carry that text as data. Not UI
# strings — nothing here is translated, it is the content being shown.
"backend/assets/samples/demo/dubbing/dubbed_zh.srt",
"backend/assets/samples/demo/dubbing/dubbed_ja.srt",
"backend/assets/samples/demo/dubbing/manifest.json",
}
def _is_allowed(rel: str) -> bool:
if rel in _ALLOWED_FILES:
return True
if any(rel.startswith(p) for p in _ALLOWED_PREFIXES):
return True
base = os.path.basename(rel)
# Test fixtures legitimately carry multilingual sample text.
if ".test." in base or base.startswith("test_") or base.endswith("_test.py"):
return True
parts = rel.split("/")
if "tests" in parts or "test" in parts:
return True
return False
def _iter_source_files():
# Scan only git-TRACKED files: the rule governs the committed codebase,
# not local untracked/vendored experiments (which also never reach CI).
import subprocess
try:
out = subprocess.run(
["git", "ls-files", "-z"],
cwd=str(_REPO), capture_output=True, text=True, timeout=30, check=True,
).stdout
names = [n for n in out.split("\0") if n]
if names:
for n in names:
if os.path.splitext(n)[1].lower() in _SKIP_EXT:
continue
yield _REPO / n
return
except Exception:
pass
# Fallback (not a git checkout): filesystem walk.
for dirpath, dirnames, filenames in os.walk(_REPO):
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
for name in filenames:
if os.path.splitext(name)[1].lower() in _SKIP_EXT:
continue
yield Path(dirpath) / name
def test_no_hardcoded_cjk_outside_locales():
offenders = {}
for path in _iter_source_files():
rel = path.relative_to(_REPO).as_posix()
if _is_allowed(rel):
continue
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
hits = [
(i + 1, line.strip()[:80])
for i, line in enumerate(text.splitlines())
if _CJK.search(line)
]
if hits:
offenders[rel] = hits
if offenders:
msg = [
"Hardcoded non-English (CJK) text found outside the translation layer.",
"Move user-facing strings into frontend/src/i18n/locales/*.json (or use English).",
"If functional CJK (regex/model-vocab/data/fixture), add the file to _ALLOWED_FILES here.",
"",
]
for rel, hits in sorted(offenders.items()):
msg.append(f" {rel}:")
for ln, snippet in hits[:3]:
msg.append(f" L{ln}: {snippet}")
pytest.fail("\n".join(msg))