Class: VideoChapterGenerationWorker

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job, Workers::StatusBroadcastable
Defined in:
app/workers/video_chapter_generation_worker.rb

Overview

Generates video chapter markers from the AssemblyAI transcript and persists
them as VideoChapter rows on the Video. Chapters are hosted-first: they feed
the on-site VideoObject "key moments" schema (VideoBasePresenter#chapter_clips)
and are independently pushable to YouTube from the CRM via
YouTube::ChapterService#push_chapters when the video is linked. Generation
needs only a structured transcript — it does NOT require a YouTube link.

Reports progress through the standard job-status page (Sidekiq::Status via
Workers::StatusBroadcastable); pass a redirect_path to send the user back
there when the job finishes. Headless callers (the transcription webhook
chain) omit it.

Instance Attribute Summary

Attributes included from Workers::StatusBroadcastable

#broadcast_status_updates

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Workers::StatusBroadcastable::Overrides

#at, #store, #total

Class Method Details

.enqueue_guarded(video, redirect_path: nil) ⇒ String?

The one guarded enqueue path — used by the CRM action, the transcription
worker (step 7) and the AssemblyAI webhook chain, so no caller can queue a
second run while one is queued/processing (the row lock serializes
concurrent checks). Marks the video queued and enqueues.

Parameters:

  • video (Video)
  • redirect_path (String, nil) (defaults to: nil)

    job-status-page redirect target on completion

Returns:

  • (String, nil)

    the Sidekiq jid, or nil when a run is already in progress



29
30
31
32
33
34
35
36
37
38
39
40
# File 'app/workers/video_chapter_generation_worker.rb', line 29

def self.enqueue_guarded(video, redirect_path: nil)
  queued = false
  video.with_lock do
    unless video.youtube_chapters_generation_in_progress?
      video.update!(youtube_chapters_generation_status: 'queued', youtube_chapters_generation_error: nil)
      queued = true
    end
  end
  return nil unless queued

  redirect_path.present? ? perform_async(video.id, redirect_path) : perform_async(video.id)
end

Instance Method Details

#perform(video_id, redirect_path = nil) ⇒ Object

Runs the job.

Parameters:

  • video_id (Integer)

    the video id

  • redirect_path (String, nil) (defaults to: nil)

    the redirect path

Returns:

  • (Object)

    the result



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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'app/workers/video_chapter_generation_worker.rb', line 47

def perform(video_id, redirect_path = nil)
  total(100)
  at(5, 'Starting chapter generation...')

  video = Video.find_by(id: video_id)
  unless video
    Rails.logger.warn("[VideoChapterGenerationWorker] Video #{video_id} not found, skipping")
    store status: 'failed'
    store error_message: "Video #{video_id} not found."
    return
  end

  unless video.has_structured_transcript_json? && video.structured_transcript_paragraphs.present?
    Rails.logger.info("[VideoChapterGenerationWorker] Video #{video_id} has no structured transcript, skipping")
    fail_job(video, 'Video has no transcript paragraphs.', redirect_path)
    return
  end

  video.update!(
    youtube_chapters_generation_status: 'processing',
    youtube_chapters_generation_error: nil
  )

  Rails.logger.info("[VideoChapterGenerationWorker] Generating chapters for video #{video_id}")
  at(20, 'Generating chapters from the transcript with AssemblyAI...')

  chapter_service = YouTube::ChapterService.new
  chapters = chapter_service.preview_chapters(video)

  if chapters.empty?
    fail_job(
      video,
      chapter_service.preview_failure_message.presence ||
        'No chapters could be generated. The transcript may be too short, or paragraph timings may be missing — re-run transcript polish, then try again.',
      redirect_path
    )
    Rails.logger.warn("[VideoChapterGenerationWorker] No chapters for video #{video_id}")
    return
  end

  at(80, 'Saving chapters...')
  chapter_service.replace_chapters!(video, chapters)
  video.update!(
    youtube_chapters_generation_status: 'complete',
    youtube_chapters_generation_error: nil
  )

  Rails.logger.info("[VideoChapterGenerationWorker] Saved #{chapters.length} chapters for video #{video_id}")
  store status: 'completed'
  at(100, "Saved #{chapters.length} chapters. Review and adjust them below.")
  store redirect_to: redirect_path if redirect_path.present?
rescue StandardError => e
  v = Video.find_by(id: video_id)
  mark_failed(v, e.message) if v
  store error_message: "Chapter generation failed: #{e.message}"
  store redirect_to: redirect_path if redirect_path.present?
  Rails.logger.error("[VideoChapterGenerationWorker] Error for video #{video_id}: #{e.message}")
  raise
end