Проверка языка результата перевода: совпадения числа абзацев недостаточно; недостающие зависимости стороннего репозитория
This commit is contained in:
@@ -111,10 +111,22 @@ ebook-convert "Название.epub" "Название.azw3"
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vetermanve/book_translator.git ~/Projects/book_translator
|
||||
~/.venvs/pdf2epub/bin/pip install openai python-dotenv # openai нет в их requirements.txt
|
||||
export DEEPSEEK_API_KEY=... # либо .env в рабочем каталоге, либо USE_OLLAMA
|
||||
# их requirements.txt неполон: openai и pyyaml импортируются, но не перечислены
|
||||
~/.venvs/pdf2epub/bin/pip install openai pyyaml python-dotenv
|
||||
|
||||
umask 077
|
||||
mkdir -p out/tr
|
||||
cat > out/tr/.env <<'EOF'
|
||||
USE_LOCAL_MODEL=false
|
||||
DEEPSEEK_API_KEY=<ключ>
|
||||
EOF
|
||||
```
|
||||
|
||||
Без `pyyaml` каждый запрос падает внутри `_create_system_prompt` **до** обращения
|
||||
к API, а сторонний скрипт молча подставляет `[UNTRANSLATED]` и рапортует
|
||||
«API запросов: 0, ошибок: N». Ключ держать только в `.env` с правами `600`,
|
||||
в командную строку и логи не выводить.
|
||||
|
||||
Сначала проверка, нужен ли перевод вообще (язык, объём; ничего не тратит):
|
||||
|
||||
```bash
|
||||
@@ -130,7 +142,19 @@ export DEEPSEEK_API_KEY=... # либо .env в рабочем каталог
|
||||
--repo ~/Projects/book_translator --workdir out/tr --workers 12
|
||||
```
|
||||
|
||||
Прерванный перевод возобновляется: сторонний скрипт помнит готовые главы.
|
||||
Прерванный перевод возобновляется: сторонний скрипт помнит готовые главы. Это
|
||||
же свойство мешает при **неудачном** прогоне — упавшие главы он считает
|
||||
готовыми, поэтому перед повторной попыткой:
|
||||
|
||||
```bash
|
||||
rm -rf out/tr/progress out/tr/context out/tr/translations
|
||||
```
|
||||
|
||||
После сборки скрипт сам проверяет язык результата и завершается с ошибкой,
|
||||
если текст остался на языке оригинала или содержит `[UNTRANSLATED]` —
|
||||
совпадения числа абзацев мало, при сбое API сторонний скрипт подставляет
|
||||
оригинал. Файл, не прошедший проверку, упаковывать нельзя.
|
||||
|
||||
Дальше `out/book.ru.html` упаковывается тем же `ebook-convert` с
|
||||
`--language=ru`.
|
||||
|
||||
|
||||
@@ -111,10 +111,16 @@ is the bridge: it writes the repo's input format, shells out to
|
||||
|
||||
It reports block/chapter/character counts and the detected source language.
|
||||
`--force` overrides the refusal.
|
||||
2. **Set up the external repo once** (a plain clone; never edit it), and its
|
||||
credentials — `DEEPSEEK_API_KEY` in the environment or a `.env` in the
|
||||
working directory, or `USE_OLLAMA` for a local model. Its `requirements.txt`
|
||||
omits `openai`, which `deepseek_translator.py` imports — install it too.
|
||||
2. **Set up the external repo once** (a plain clone; never edit it) and its
|
||||
credentials — a `.env` in the working directory with `USE_LOCAL_MODEL=false`
|
||||
and `DEEPSEEK_API_KEY=…`, or `USE_LOCAL_MODEL=true` plus `OLLAMA_MODEL` for a
|
||||
local model. **Its `requirements.txt` is incomplete** — `pip install openai
|
||||
pyyaml python-dotenv` as well. Both `openai` and `pyyaml` are imported at
|
||||
runtime and missing from that file; without `pyyaml` every single request
|
||||
dies inside `_create_system_prompt` *before* reaching the API, and the tool
|
||||
reports "API запросов: 0, ошибок: N" while writing `[UNTRANSLATED]` stubs.
|
||||
Write the `.env` under `umask 077`, and never echo the key into logs or
|
||||
command output.
|
||||
3. **Translate:**
|
||||
|
||||
`python scripts/translate.py book.html book.ru.html --repo <clone> --workdir <dir> --workers 12`
|
||||
@@ -126,7 +132,15 @@ is the bridge: it writes the repo's input format, shells out to
|
||||
потеряна" counts paragraphs where the model mangled the inline-tag markers
|
||||
and the italics were dropped rather than corrupted. Both are expected to be
|
||||
near zero — a large number means the translator misbehaved, not that the
|
||||
bridge is broken.
|
||||
bridge is broken. The script then checks the output's actual language and
|
||||
exits non-zero if the text is still the source language or contains
|
||||
`[UNTRANSLATED]` stubs — **matching paragraph counts do not prove anything
|
||||
was translated**, since the external tool substitutes the original on API
|
||||
failure. Do not pack a file that failed this check.
|
||||
**Re-running after a failure needs the state cleared:** the external repo's
|
||||
`progress/` directory marks those chapters complete and will skip them.
|
||||
Delete `<workdir>/progress`, `<workdir>/context`, and
|
||||
`<workdir>/translations` before the retry.
|
||||
5. Pack `book.ru.html` with `--language=ru` and translated `--title`/`--authors`.
|
||||
|
||||
How formatting survives a translator that only speaks plain text: inline `<i>`
|
||||
|
||||
@@ -71,6 +71,21 @@ def test_split_and_rebuild(tmp: Path):
|
||||
assert result.count("<h1>") == 2
|
||||
|
||||
|
||||
def test_verify_output_catches_untranslated(tmp: Path):
|
||||
"""Регресс: первый боевой прогон отрапортовал успех на непереведённом файле."""
|
||||
bad = tmp / "bad.html"
|
||||
bad.write_text("<p>[UNTRANSLATED] The build is broken.</p>\n" * 30, encoding="utf-8")
|
||||
assert not t.verify_output(bad, "ru"), "заглушки должны заваливать проверку"
|
||||
|
||||
english = tmp / "en.html"
|
||||
english.write_text("<p>The build is broken again today.</p>\n" * 40, encoding="utf-8")
|
||||
assert not t.verify_output(english, "ru"), "английский результат должен заваливать проверку"
|
||||
|
||||
good = tmp / "ru.html"
|
||||
good.write_text("<p>Сборка снова сломалась сегодня утром.</p>\n" * 40, encoding="utf-8")
|
||||
assert t.verify_output(good, "ru")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tempfile
|
||||
|
||||
@@ -79,4 +94,6 @@ if __name__ == "__main__":
|
||||
test_language_detection()
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
test_split_and_rebuild(Path(d))
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
test_verify_output_catches_untranslated(Path(d))
|
||||
print("OK")
|
||||
|
||||
+23
-1
@@ -146,6 +146,24 @@ def rebuild(src, chapters, workdir, out):
|
||||
return missing == 0
|
||||
|
||||
|
||||
def verify_output(out, target):
|
||||
"""Совпадение числа абзацев ещё не значит, что перевод состоялся: внешний
|
||||
скрипт при ошибке API молча подставляет оригинал. Проверяем язык результата.
|
||||
"""
|
||||
text = re.sub("<[^>]+>", "", out.read_text(encoding="utf-8"))
|
||||
lang, share = detect_language(text)
|
||||
stub = text.count("[UNTRANSLATED]")
|
||||
print("результат: %s (кириллица %.0f%%), заглушек [UNTRANSLATED]: %d"
|
||||
% (lang, share * 100, stub))
|
||||
if stub:
|
||||
print("ВНИМАНИЕ: внешний переводчик вернул заглушки — проверь его логи и ключ API")
|
||||
return False
|
||||
if lang != target:
|
||||
print("ВНИМАНИЕ: результат не на языке %s — перевода фактически не было" % target)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("source", type=Path, help="XHTML от pdf2html.py")
|
||||
@@ -184,7 +202,11 @@ def main():
|
||||
extracted = (args.workdir / "extracted").resolve()
|
||||
write_input(chapters, extracted)
|
||||
run_translator(args.repo.resolve(), args.workdir.resolve(), extracted, args.workers)
|
||||
rebuild(args.source, chapters, args.workdir, args.output)
|
||||
ok = rebuild(args.source, chapters, args.workdir, args.output)
|
||||
if not verify_output(args.output, args.target):
|
||||
sys.exit("перевод не состоялся — упаковывать этот файл нельзя")
|
||||
if not ok:
|
||||
print("часть блоков осталась на языке оригинала — см. счётчик выше")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user