Repository navigation
Expand file tree
/
Copy pathbase_caption.py
More file actions
198 lines (163 loc) · 6.85 KB
/
Copy pathbase_caption.py
File metadata and controls
198 lines (163 loc) · 6.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# BaseCaption
# Copyright (C) 2024-2026 Collabora Ltd.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
import backend
from backend import GObject, analytics, frameio
from video_transform import VideoTransform
# The text pad is a GStreamer request pad: a hosted element on g2g has one
# source pad and stages its caption as metadata instead.
if backend.BACKEND == "gst":
import gi
gi.require_version("Gst", "1.0")
gi.require_version("GstBase", "1.0")
from gi.repository import Gst, GstBase # noqa: E402
TEXT_CAPS = Gst.Caps.from_string("text/x-raw, format=utf8")
#: How long a caption stays on screen. Long enough that the subtitle is still up
#: when the next frame's caption arrives.
CAPTION_DURATION_SECONDS = 60
NANOSECONDS_PER_SECOND = 1_000_000_000
class BaseCaption(VideoTransform):
"""
Base element for captioning video frames.
"""
__gstmetadata__ = (
"BaseCaption",
"Transform",
"Captions video clips",
"Aaron Boxer <aaron.boxer@collabora.com>",
)
if backend.BACKEND == "gst":
__gsttemplates__ = (
Gst.PadTemplate.new(
"text_src", Gst.PadDirection.SRC, Gst.PadPresence.REQUEST, TEXT_CAPS
),
)
def __init__(self):
super().__init__()
self._prompt = "What is shown in this image?"
self.text_src_pad = None
@GObject.Property(type=str)
def system_prompt(self):
"Custom system prompt text"
return self._system_prompt
@system_prompt.setter
def system_prompt(self, value):
self._system_prompt = value
@GObject.Property(type=str)
def prompt(self):
"Custom prompt text"
return self._prompt
@prompt.setter
def prompt(self, value):
self._prompt = value
# make read only
@GObject.Property(type=str)
def engine_name(self):
"Machine Learning Engine to use : pytorch, tflite, tensorflow, onnx or openvino, or custom engine name"
return self.mgr.engine_name
@engine_name.setter
def engine_name(self, value):
raise ValueError(
"The 'engine_name' property cannot be set in this derived class."
)
def forward(self, frames):
return self.engine.do_forward(frames) if self.engine else None
def process_frames(self, frames, num_sources, fmt, target):
"""Caption each source, staging one classification per caption.
Captions carry no pixels and no blob, so this replaces the shared
infer-decode-write body rather than filling in `decode`.
"""
result = self.forward(frames)
if result is None:
raise RuntimeError(f"{type(self).__name__}: captioning returned None")
captions = result if isinstance(result, list) else [result] * num_sources
if len(captions) != num_sources:
raise RuntimeError(f"expected {num_sources} captions, got {len(captions)}")
meta = analytics.add_relation_meta(target)
if meta is None:
self.logger.error("Failed to add analytics metadata to buffer")
return
for index, caption in enumerate(captions):
if not caption:
self.logger.warning(f"stream {index}: no caption generated")
continue
label = caption if num_sources == 1 else f"stream_{index}_{caption}"
if analytics.add_classification(meta, index, label) is None:
self.logger.error(f"stream {index}: failed to add the caption")
else:
self.logger.info(f"stream {index}: added caption {caption}")
self.push_captions(captions, target)
def push_captions(self, captions, buf):
"""Send each caption out the text pad, one buffer per source.
The caption is staged as metadata either way, so the pad is an extra:
nothing to do unless something asked for it, which is also what makes
this a no-op when no text pad is linked.
"""
if backend.BACKEND == "g2g":
for caption in captions:
if caption:
frameio.emit_text(
caption, CAPTION_DURATION_SECONDS * NANOSECONDS_PER_SECOND
)
return
if self.text_src_pad is None:
return
if buf.pts == Gst.CLOCK_TIME_NONE:
buf.pts = Gst.util_uint64_scale(
Gst.util_get_timestamp(),
1, # framerate_denom
30 * Gst.SECOND, # framerate_num
)
if buf.duration == Gst.CLOCK_TIME_NONE:
buf.duration = Gst.SECOND // 30 # framerate_num
share = buf.duration // len(captions)
for index, caption in enumerate(captions):
if caption:
self.push_text_buffer(caption, buf.pts + index * share, buf.dts)
def push_text_buffer(self, text, pts, dts):
"""Push one caption to the `text_src` pad, timed with its video frame."""
text_buffer = Gst.Buffer.new_wrapped(text.encode("utf-8"))
text_buffer.pts = pts
text_buffer.dts = dts
text_buffer.duration = CAPTION_DURATION_SECONDS * Gst.SECOND
ret = self.text_src_pad.push(text_buffer)
if ret != Gst.FlowReturn.OK:
self.logger.warning(f"Failed to push text buffer: {ret}")
def do_request_new_pad(self, template, name, caps):
if self.text_src_pad:
self.logger.error("Element already has a text_src")
return None
if name != template.name_template:
self.logger.error("Invalid pad name")
return None
self.text_src_pad = Gst.Pad.new_from_template(template, name)
self.add_pad(self.text_src_pad)
self.text_src_pad.set_active(True)
return self.text_src_pad
def do_release_pad(self, pad):
self.remove_pad(pad)
pad.set_active(False)
self.text_src_pad = None
def do_sink_event(self, event):
if self.text_src_pad:
text_event = (
Gst.Event.new_caps(TEXT_CAPS)
if event.type == Gst.EventType.CAPS
else event
)
self.text_src_pad.push_event(text_event)
return GstBase.BaseTransform.do_sink_event(self, event)