Проверка языка результата перевода: совпадения числа абзацев недостаточно; недостающие зависимости стороннего репозитория
This commit is contained in:
@@ -111,10 +111,22 @@ ebook-convert "Название.epub" "Название.azw3"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/vetermanve/book_translator.git ~/Projects/book_translator
|
git clone https://github.com/vetermanve/book_translator.git ~/Projects/book_translator
|
||||||
~/.venvs/pdf2epub/bin/pip install openai python-dotenv # openai нет в их requirements.txt
|
# их requirements.txt неполон: openai и pyyaml импортируются, но не перечислены
|
||||||
export DEEPSEEK_API_KEY=... # либо .env в рабочем каталоге, либо USE_OLLAMA
|
~/.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
|
```bash
|
||||||
@@ -130,7 +142,19 @@ export DEEPSEEK_API_KEY=... # либо .env в рабочем каталог
|
|||||||
--repo ~/Projects/book_translator --workdir out/tr --workers 12
|
--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` с
|
Дальше `out/book.ru.html` упаковывается тем же `ebook-convert` с
|
||||||
`--language=ru`.
|
`--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.
|
It reports block/chapter/character counts and the detected source language.
|
||||||
`--force` overrides the refusal.
|
`--force` overrides the refusal.
|
||||||
2. **Set up the external repo once** (a plain clone; never edit it), and its
|
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
|
credentials — a `.env` in the working directory with `USE_LOCAL_MODEL=false`
|
||||||
working directory, or `USE_OLLAMA` for a local model. Its `requirements.txt`
|
and `DEEPSEEK_API_KEY=…`, or `USE_LOCAL_MODEL=true` plus `OLLAMA_MODEL` for a
|
||||||
omits `openai`, which `deepseek_translator.py` imports — install it too.
|
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:**
|
3. **Translate:**
|
||||||
|
|
||||||
`python scripts/translate.py book.html book.ru.html --repo <clone> --workdir <dir> --workers 12`
|
`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
|
потеряна" counts paragraphs where the model mangled the inline-tag markers
|
||||||
and the italics were dropped rather than corrupted. Both are expected to be
|
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
|
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`.
|
5. Pack `book.ru.html` with `--language=ru` and translated `--title`/`--authors`.
|
||||||
|
|
||||||
How formatting survives a translator that only speaks plain text: inline `<i>`
|
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
|
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__":
|
if __name__ == "__main__":
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
@@ -79,4 +94,6 @@ if __name__ == "__main__":
|
|||||||
test_language_detection()
|
test_language_detection()
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
test_split_and_rebuild(Path(d))
|
test_split_and_rebuild(Path(d))
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
test_verify_output_catches_untranslated(Path(d))
|
||||||
print("OK")
|
print("OK")
|
||||||
|
|||||||
+23
-1
@@ -146,6 +146,24 @@ def rebuild(src, chapters, workdir, out):
|
|||||||
return missing == 0
|
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():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("source", type=Path, help="XHTML от pdf2html.py")
|
ap.add_argument("source", type=Path, help="XHTML от pdf2html.py")
|
||||||
@@ -184,7 +202,11 @@ def main():
|
|||||||
extracted = (args.workdir / "extracted").resolve()
|
extracted = (args.workdir / "extracted").resolve()
|
||||||
write_input(chapters, extracted)
|
write_input(chapters, extracted)
|
||||||
run_translator(args.repo.resolve(), args.workdir.resolve(), extracted, args.workers)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user