Skip to content

Commit 1d7f4de

Browse files
committed
refactor transcript serializer
1 parent 0059582 commit 1d7f4de

File tree

11 files changed

+123
-144
lines changed

11 files changed

+123
-144
lines changed

Gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,4 @@ gem "dry-types", "~> 1.7"
115115

116116
gem "google-protobuf", require: false
117117

118-
gem "webvtt-ruby"
119-
120118
gem "active_job-performs", "~> 0.3.1"

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,6 @@ GEM
441441
websocket-driver (0.7.6)
442442
websocket-extensions (>= 0.1.0)
443443
websocket-extensions (0.1.5)
444-
webvtt-ruby (0.4.2)
445444
xpath (3.2.0)
446445
nokogiri (~> 1.8)
447446
zeitwerk (2.6.16)
@@ -501,7 +500,6 @@ DEPENDENCIES
501500
vite_rails
502501
web-console
503502
webmock
504-
webvtt-ruby
505503

506504
RUBY VERSION
507505
ruby 3.3.1p55

app/clients/youtube/transcript.rb

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Youtube
44
class Transcript
55
attr_reader :response
66

7-
def get_vtt(video_id)
7+
def get(video_id)
88
message = {one: "asr", two: "en"}
99
typedef = MessageType
1010
two = get_base64_protobuf(message, typedef)
@@ -25,11 +25,11 @@ def get_vtt(video_id)
2525
}
2626

2727
@response = HTTParty.post(url, headers: headers, body: body.to_json)
28-
convert_to_vtt(JSON.parse(response.body))
28+
JSON.parse(@response.body)
2929
end
3030

31-
def self.get_vtt(video_id)
32-
new.get_vtt(video_id)
31+
def self.get(video_id)
32+
new.get(video_id)
3333
end
3434

3535
private
@@ -43,32 +43,5 @@ def get_base64_protobuf(message, typedef)
4343
encoded_data = encode_message(message, typedef)
4444
Base64.encode64(encoded_data).delete("\n")
4545
end
46-
47-
def convert_to_vtt(transcript)
48-
vtt_content = "WEBVTT\n\n"
49-
events = transcript.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", "initialSegments")
50-
if events
51-
events.each_with_index do |event, index|
52-
segment = event["transcriptSegmentRenderer"]
53-
start_time = format_time(segment["startMs"].to_i)
54-
end_time = format_time(segment["endMs"].to_i)
55-
text = segment.dig("snippet", "runs")&.map { |run| run["text"] }&.join || ""
56-
vtt_content += "#{index + 1}\n"
57-
vtt_content += "#{start_time} --> #{end_time}\n"
58-
vtt_content += "#{text}\n\n"
59-
end
60-
else
61-
vtt_content += "NOTE No transcript data available\n"
62-
end
63-
vtt_content
64-
end
65-
66-
def format_time(ms)
67-
hours = ms / (1000 * 60 * 60)
68-
minutes = (ms % (1000 * 60 * 60)) / (1000 * 60)
69-
seconds = (ms % (1000 * 60)) / 1000
70-
milliseconds = ms % 1000
71-
format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds)
72-
end
7346
end
7447
end

app/models/cue.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class Cue
2+
attr_reader :start_time, :end_time, :text
3+
4+
def initialize(start_time, end_time, text)
5+
@start_time = start_time
6+
@end_time = end_time
7+
@text = text
8+
end
9+
10+
def to_s
11+
"#{start_time} --> #{end_time}\n#{text}"
12+
end
13+
14+
def to_h
15+
{
16+
start_time: start_time,
17+
end_time: end_time,
18+
text: text
19+
}
20+
end
21+
end

app/models/talk.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Talk < ApplicationRecord
3838
has_many :speaker_talks, dependent: :destroy, inverse_of: :talk, foreign_key: :talk_id
3939
has_many :speakers, through: :speaker_talks
4040

41-
serialize :transcript, coder: WebVTTSerializer
41+
serialize :transcript, coder: TranscriptSerializer
4242

4343
# validations
4444
validates :title, presence: true
@@ -129,6 +129,7 @@ def related_talks(limit: 6)
129129
end
130130

131131
def update_transcript!
132-
update!(transcript: Youtube::Transcript.get_vtt(video_id))
132+
youtube_transcript = Youtube::Transcript.get(video_id)
133+
update!(transcript: Transcript.create_from_youtube_transcript(youtube_transcript))
133134
end
134135
end

app/models/transcript.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Transcript
2+
include Enumerable
3+
4+
attr_reader :cues
5+
6+
def initialize
7+
@cues = []
8+
end
9+
10+
def add_cue(cue)
11+
@cues << cue
12+
end
13+
14+
def to_h
15+
@cues.map { |cue| cue.to_h }
16+
end
17+
18+
def to_json
19+
to_h.to_json
20+
end
21+
22+
def to_vtt
23+
vtt_content = "WEBVTT\n\n"
24+
@cues.each_with_index do |cue, index|
25+
vtt_content += "#{index + 1}\n"
26+
vtt_content += "#{cue}\n\n"
27+
end
28+
vtt_content
29+
end
30+
31+
def each(&)
32+
@cues.each(&)
33+
end
34+
35+
class << self
36+
def create_from_youtube_transcript(youtube_transcript)
37+
transcript = Transcript.new
38+
events = youtube_transcript.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", "initialSegments")
39+
if events
40+
events.each do |event|
41+
segment = event["transcriptSegmentRenderer"]
42+
start_time = format_time(segment["startMs"].to_i)
43+
end_time = format_time(segment["endMs"].to_i)
44+
text = segment.dig("snippet", "runs")&.map { |run| run["text"] }&.join || ""
45+
transcript.add_cue(Cue.new(start_time, end_time, text))
46+
end
47+
else
48+
transcript.add_cue(Cue.new("00:00:00.000", "00:00:00.000", "NOTE No transcript data available"))
49+
end
50+
transcript
51+
end
52+
53+
def format_time(ms)
54+
hours = ms / (1000 * 60 * 60)
55+
minutes = (ms % (1000 * 60 * 60)) / (1000 * 60)
56+
seconds = (ms % (1000 * 60)) / 1000
57+
milliseconds = ms % 1000
58+
format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds)
59+
end
60+
end
61+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class TranscriptSerializer
2+
def self.dump(transcript)
3+
transcript.to_json
4+
end
5+
6+
def self.load(transcript_json)
7+
return nil if transcript_json.nil? || transcript_json.empty?
8+
9+
cues_array = JSON.parse(transcript_json, symbolize_names: true)
10+
transcript = Transcript.new
11+
cues_array.each do |cue_hash|
12+
transcript.add_cue(Cue.new(cue_hash[:start_time], cue_hash[:end_time], cue_hash[:text]))
13+
end
14+
transcript
15+
end
16+
end

app/serializers/webvtt_serializer.rb

Lines changed: 0 additions & 32 deletions
This file was deleted.

config/initializers/inflections.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@
1212

1313
# These inflection rules are supported but not enabled by default:
1414
ActiveSupport::Inflector.inflections(:en) do |inflect|
15-
inflect.acronym "WebVTT"
1615
end
Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,20 @@
11
require "test_helper"
2-
require "webvtt"
32

4-
module Youtube
5-
class TranscriptTest < ActiveSupport::TestCase
6-
def setup
7-
@client = Youtube::Transcript.new
8-
end
9-
10-
test "fetch the trasncript from a video in vtt format" do
11-
video_id = "9LfmrkyP81M"
12-
13-
VCR.use_cassette("youtube_video_transcript", match_requests_on: [:method]) do
14-
transcript = @client.get_vtt(video_id)
15-
assert_not_nil transcript
16-
17-
# Save the VTT content to a temporary file to parse it using WebVTT gem
18-
Tempfile.create(["transcript", ".vtt"]) do |file|
19-
file.write(transcript)
20-
file.rewind
21-
22-
# Parse the VTT file
23-
webvtt = WebVTT.read(file.path)
3+
class Youtube::TranscriptTest < ActiveSupport::TestCase
4+
def setup
5+
@client = Youtube::Transcript.new
6+
end
247

25-
# Ensure it has the correct headers
26-
assert_match(/^WEBVTT/, transcript)
8+
test "fetch the trasncript from a video in vtt format" do
9+
video_id = "9LfmrkyP81M"
2710

28-
# Ensure it has at least one cue
29-
assert_not_empty webvtt.cues
11+
VCR.use_cassette("youtube_video_transcript", match_requests_on: [:method]) do
12+
transcript = @client.get(video_id)
13+
assert_not_nil transcript
3014

31-
# Validate each cue
32-
webvtt.cues.each do |cue|
33-
assert_not_nil cue.start
34-
assert_not_nil cue.end
35-
assert_not_nil cue.text
36-
assert_match(/^\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3}$/, "#{cue.start} --> #{cue.end}")
37-
end
38-
end
39-
end
15+
transcript = Transcript.create_from_youtube_transcript(transcript)
16+
assert_not_empty transcript.cues
17+
assert transcript.cues.first.is_a?(Cue)
4018
end
4119
end
4220
end

test/models/talk_test.rb

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,13 @@
2727

2828
class TalkTest < ActiveSupport::TestCase
2929
include ActiveJob::TestHelper
30-
test "should serialize and deserialize transcript correctly" do
31-
vtt_string = <<~VTT
32-
WEBVTT
33-
34-
00:00.000 --> 00:05.000
35-
Welcome to the talk.
36-
37-
00:06.000 --> 00:10.000
38-
Let's get started.
39-
VTT
40-
41-
talk = Talk.new(title: "Sample Talk", transcript: vtt_string)
42-
assert talk.save
43-
44-
loaded_talk = Talk.find(talk.id)
45-
assert_equal WebVTTSerializer.load(vtt_string), loaded_talk.transcript
46-
end
47-
48-
test "should convert transcript to WebVTT format correctly" do
49-
vtt_string = <<~VTT
50-
WEBVTT
51-
52-
00:00.000 --> 00:05.000
53-
Welcome to the talk.
54-
55-
00:06.000 --> 00:10.000
56-
Let's get started.
57-
VTT
58-
59-
cues = WebVTTSerializer.load(vtt_string)
60-
talk = Talk.new(title: "Sample Talk", transcript: cues)
61-
62-
expected_vtt = WebVTTSerializer.dump(talk.transcript)
63-
assert_equal vtt_string.strip, expected_vtt.strip
64-
end
65-
6630
test "should handle empty transcript" do
67-
talk = Talk.new(title: "Sample Talk", transcript: [])
31+
talk = Talk.new(title: "Sample Talk", transcript: Transcript.new)
6832
assert talk.save
6933

7034
loaded_talk = Talk.find(talk.id)
71-
assert_empty loaded_talk.transcript
35+
assert_equal loaded_talk.transcript.cues, []
36+
assert_equal "Sample Talk", loaded_talk.title
7237
end
7338

7439
test "should update transcript" do
@@ -80,7 +45,8 @@ class TalkTest < ActiveSupport::TestCase
8045
end
8146
end
8247

83-
assert_not_empty @talk.transcript
84-
assert @talk.transcript.length > 100
48+
assert @talk.transcript.is_a?(Transcript)
49+
assert @talk.transcript.cues.first.is_a?(Cue)
50+
assert @talk.transcript.cues.length > 100
8551
end
8652
end

0 commit comments

Comments
 (0)