Class: Publication::ImageTextTranslator

Inherits:
Object
  • Object
show all
Defined in:
app/services/publication/image_text_translator.rb

Overview

Translates text rasterized INTO a PDF's images — the part document
text-layer translators cannot reach: spec callouts, wiring diagrams,
marketing imagery with baked-in copy.

Pipeline per document:

  1. Pdf::ImageExtractor pulls every painted image with its on-page bbox.
  2. A vision call (+AiModelConstants+ +:vision_analysis+) decides whether
    the image actually contains readable source-language text.
  3. Flagged images are regenerated by the image-variation engine
    (ImageGeneration::Service#generate_with_data) with the text
    translated, then stamped back over the EXACT original bbox via
    Pdf::Toolkit.stamp — layout is untouched.

Every step is captured in the +report+ so the PDF-studio reviewer can see
what was replaced, what was skipped and why. Capped at MAX_IMAGES
regenerated images per document — beyond that the doc needs human art
direction, not a bulk machine pass.

Examples:

result = Publication::ImageTextTranslator.call('/tmp/manual-fr.pdf', lang_in: 'en', lang_out: 'fr')
result.bytes  # => "%PDF-1.7…" (images swapped)
result.report # => [{ page:, bbox:, status: 'replaced'|'no_text'|…, … }]

Defined Under Namespace

Classes: Result

Constant Summary collapse

MAX_IMAGES =

Max images regenerated per document (each is a paid image-gen call).

10
DETECTION_PROMPT =
<<~PROMPT
    You are inspecting one image extracted from a %<lang_in>s product document.
    Does it contain readable, meaningful text in %<lang_in>s (labels, callouts,
    captions, table text, warnings)? Ignore: pure photography, logos consisting
    only of a brand wordmark, and images with no legible words.
    Answer with ONLY a JSON object, no markdown fences:
    {"contains_text": true|false, "text": "all visible text, verbatim", "kind": "diagram|table|chart|photo|other"}
PROMPT

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(lang_in:, lang_out:) ⇒ ImageTextTranslator

Returns a new instance of ImageTextTranslator.



51
52
53
54
55
# File 'app/services/publication/image_text_translator.rb', line 51

def initialize(lang_in:, lang_out:)
  @lang_in  = lang_in
  @lang_out = lang_out
  @service  = ImageGeneration::Service.new
end

Class Method Details

.call(pdf_path, lang_in:, lang_out:) ⇒ Result

Returns new PDF bytes + per-image report.

Parameters:

  • pdf_path (String)

    local path of the (already text-translated) PDF

  • lang_in (String)

    source language code

  • lang_out (String)

    target language code

Returns:

  • (Result)

    new PDF bytes + per-image report



46
47
48
# File 'app/services/publication/image_text_translator.rb', line 46

def call(pdf_path, lang_in:, lang_out:)
  new(lang_in:, lang_out:).call(pdf_path)
end

Instance Method Details

#call(pdf_path) ⇒ Object



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
# File 'app/services/publication/image_text_translator.rb', line 57

def call(pdf_path)
  extraction = Pdf::ImageExtractor.extract(pdf_path)
  report     = []
  swaps      = []
  regenerated = 0

  extraction.placements.each do |placement|
    detection = detect_text(placement)
    unless detection[:contains_text]
      report << entry(placement, 'no_text')
      next
    end

    if regenerated >= MAX_IMAGES
      report << entry(placement, 'skipped_limit', detection)
      next
    end

    new_path = regenerate(placement, detection)
    if new_path
      swaps << [placement, new_path]
      regenerated += 1
      report << entry(placement, 'replaced', detection)
    else
      report << entry(placement, 'regeneration_failed', detection)
    end
  end

  bytes = swaps.empty? ? File.binread(pdf_path) : apply_swaps(pdf_path, swaps)
  Result.new(bytes:, report:)
ensure
  FileUtils.rm_rf(extraction.workdir) if extraction
end