Class: PublicationTranslationWorker

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job, Sidekiq::Status::Worker
Defined in:
app/workers/publication_translation_worker.rb

Overview

Translates a publication's PDF into another language via DeepLClient and
stages the result as a GeneratedPdf (kind +translated+) for human review in
the PDF studio. On import
(GeneratedPdfImporter, mode :new with locales) the translated PDF becomes a
sibling publication Item, gets a RelatedPublication translation link back to
the source, and — because import routes through Publication::Saver — the
full extraction/vision/embedding pipeline fires for the translated document
for free (Sunny can then search it in the target language).

Enqueued by Sunny's +pdf_translate+ tool and the CRM publication +translate+
action:

Design: doc/tasks/202607201903_PDF_TRANSLATION_POWERHOUSE.md.

Examples:

PublicationTranslationWorker.perform_async(publication.id, 'fr', 'created_by_id' => .id)

Constant Summary collapse

ALLOWED_SOURCE_RECORDS =

Only these classes may be linked as the review context — never constantize
an arbitrary Sidekiq arg.

%w[AssistantConversation Item].freeze

Instance Method Summary collapse

Instance Method Details

#perform(publication_id, lang_out, options = {}) ⇒ Object

Parameters:

  • publication_id (Integer)

    the publication Item to translate

  • lang_out (String)

    target language code (e.g. 'fr')

  • options (Hash) (defaults to: {})

Options Hash (options):

  • 'created_by_id' (Integer)

    Account id recorded on the staged PDF

  • 'source_record_type' (String)
    • 'source_record_id' polymorphic
      review context (AssistantConversation from Sunny, or the source Item)
  • 'instructions' (String)

    free-text note stored on the staged PDF



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
# File 'app/workers/publication_translation_worker.rb', line 36

def perform(publication_id, lang_out, options = {})
  publication = Item.publications.find_by(id: publication_id)
  return Rails.logger.info("[PublicationTranslationWorker] Publication #{publication_id} not found, skipping") unless publication
  return Rails.logger.info("[PublicationTranslationWorker] Publication #{publication_id} is discontinued, skipping") if publication.is_discontinued?

  literature = publication.literature
  return Rails.logger.info("[PublicationTranslationWorker] Publication #{publication_id} has no literature, skipping") unless literature&.attachment_stored?

  conversation = source_record(options)
  begin
    lang_out = DeepLClient.canonical_document_locale(lang_out)
    result = translate_document(literature, lang_out, lang_in: deepl_source_lang_in(publication))
  rescue DeepLClient::UnsupportedLanguage => e
    # A retry cannot make an unsupported target valid. Timeouts deliberately
    # escape this rescue: DeepL keeps processing remotely, and Sidekiq resumes
    # the stored document on its next attempt.
    ErrorReporting.warning(e, source: :background, publication_id:, sku: publication.sku, lang_out:)
    store error_message: failure_reason(e)
    notify_conversation(conversation, failure_message(publication, lang_out, e))
    return
  end

  # Phase 2 (opt-in): translate text baked INTO the images. Non-fatal — a
  # failure here keeps the text-layer translation rather than losing the doc.
  image_report = nil
  if options['translate_images'].to_b
    bytes, image_report = translate_image_text(result.bytes, lang_out:)
    result = DeepLClient::Result.new(bytes:, meta: result.meta)
  end

  base = publication.publication_base_name.presence || publication.sku
  # The catalog metadata (name/slug derive from the base name) is translated
  # too — a French variant files under a French name, not the English name
  # with a locale suffix. Non-fatal: falls back to the source base name.
  translated_base = Publication::MetadataTranslator.call(base, lang_out:, lang_in: source_lang_in(publication))
  title = translated_base.presence || "#{base} (#{lang_out.to_s.upcase})"
  staged = GeneratedPdfGenerator.stage(
    bytes:                 result.bytes,
    layout:                translation_layout(result.meta, publication, image_report:, translated_base:),
    kind:                  'translated',
    title:,
    instructions:          options['instructions'],
    created_by_id:         options['created_by_id'],
    source_publication_id: publication.id,
    source_record:         conversation,
    filename:              "#{(translated_base.presence || base).to_s.parameterize}-#{lang_out.downcase}.pdf"
  )
  raise staged.error.presence || 'staging failed' unless staged.success?

  # The job monitor redirects here on completion (jobs#show reads redirect_to).
  store redirect_to: "#{CRM_URL}/pdf_studio/#{staged.generated_pdf.id}",
        info_message: "Translation staged for review — #{publication.sku}#{lang_out}"
  notify_conversation(conversation, success_message(publication, lang_out, staged.generated_pdf, result.meta))
  Rails.logger.info("[PublicationTranslationWorker] Publication #{publication_id} translated to #{lang_out}: staged GeneratedPdf #{staged.generated_pdf.id}")
end