Class: YouTube::ChapterService

Inherits:
Object
  • Object
show all
Defined in:
app/services/youtube/chapter_service.rb

Overview

Generates YouTube-style video chapters from structured transcript paragraphs
using AssemblyAI LLM Gateway (semantic boundaries + titles), then pushes
timestamp lines to the YouTube video description.

YouTube chapter format requirements:

  • First chapter must be at 0:00
  • Minimum 3 chapters
  • Each chapter at least 10 seconds long (gap between consecutive starts)
  • Timestamps in chronological order in the description

rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- LLM + validation pipeline

Constant Summary collapse

MIN_CHAPTERS =
3
MAX_CHAPTERS =

Hard cap for YouTube + LLM schema; prompt steers the model toward fewer (see build_semantic_prompt).

12
MIN_CHAPTER_DURATION_SECONDS =
10
PARAGRAPHS_PER_SEGMENT =

Keep prompts within a single LLM call; long transcripts use multiple segments.

48
PARAGRAPH_TEXT_MAX =
400
SEMANTIC_TOOL_NAME =
'youtube_semantic_chapters'
LLM_MAX_TOKENS =
4096

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(account: nil, yt_client: nil) ⇒ ChapterService

Returns a new instance of ChapterService.



28
29
30
31
32
# File 'app/services/youtube/chapter_service.rb', line 28

def initialize(account: nil, yt_client: nil)
  @yt_client = yt_client || YouTube::ApiClient.new(account: )
  @logger = Rails.logger
  @preview_failure_message = nil
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



26
27
28
# File 'app/services/youtube/chapter_service.rb', line 26

def logger
  @logger
end

#preview_failure_messageObject (readonly)

Returns the value of attribute preview_failure_message.



26
27
28
# File 'app/services/youtube/chapter_service.rb', line 26

def preview_failure_message
  @preview_failure_message
end

#yt_clientObject (readonly)

Returns the value of attribute yt_client.



26
27
28
# File 'app/services/youtube/chapter_service.rb', line 26

def yt_client
  @yt_client
end

Instance Method Details

#generate_and_push(video) ⇒ Array<Hash>

Generate chapters and push to YouTube description.

Parameters:

  • video (Video)

    must have youtube_id and structured_transcript_json with paragraphs

Returns:

  • (Array<Hash>)

    the generated chapters [{ timestamp:, title:, start_ms: }, ...]

Raises:

  • (ArgumentError)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'app/services/youtube/chapter_service.rb', line 37

def generate_and_push(video)
  validate!(video)

  paragraphs = video.structured_transcript_paragraphs
  raise ArgumentError, 'Video has no paragraphs in structured transcript' if paragraphs.blank?

  chapters = generate_chapters(paragraphs, video.duration_in_seconds)
  if chapters.length < MIN_CHAPTERS
    logger.warn(
      "[YouTube::ChapterService] Skipping YouTube description update for video #{video.id}: " \
      "need at least #{MIN_CHAPTERS} chapters, got #{chapters.length}. " \
      'YouTube requires chapters at 0:00, at least 3 markers, and each segment ≥10 seconds.'
    )
    return []
  end

  push_chapters_to_description(video, chapters)
  chapters
end

#preview_chapters(video) ⇒ Array<Hash>

Generate chapters without pushing to YouTube (preview).

Parameters:

Returns:

  • (Array<Hash>)

    the generated chapters



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/services/youtube/chapter_service.rb', line 60

def preview_chapters(video)
  @preview_failure_message = nil
  paragraphs = video.structured_transcript_paragraphs
  if paragraphs.blank?
    @preview_failure_message = 'No transcript paragraphs in structured transcript.'
    return []
  end

  chapters = generate_chapters(paragraphs, video.duration_in_seconds)
  if chapters.empty? && @preview_failure_message.blank?
    @preview_failure_message = 'No chapters passed YouTube rules (minimum three markers, at least ten seconds apart).'
  end
  chapters
end

#push_prepared_chapters(video, chapters) ⇒ Object

Push chapters already produced by #preview_chapters (same hash shape) without re-running the LLM.

Parameters:

  • chapters (Array<Hash>)

    each with :timestamp, :title, :start_ms (string or symbol keys OK)

  • video (Object)

Raises:

  • (ArgumentError)


78
79
80
81
82
83
84
85
# File 'app/services/youtube/chapter_service.rb', line 78

def push_prepared_chapters(video, chapters)
  validate!(video)

  list = Array(chapters).filter_map { |c| normalize_chapter_hash_for_push(c) }
  raise ArgumentError, 'No chapters to push' if list.blank?

  push_chapters_to_description(video, list)
end