screenkit 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/docker.yml +44 -0
- data/.github/workflows/ruby-tests.yml +21 -1
- data/Brewfile +5 -0
- data/CHANGELOG.md +13 -1
- data/CONTRIBUTING.md +25 -0
- data/DOCUMENTATION.md +132 -48
- data/Dockerfile +107 -0
- data/lib/screen_kit.rb +17 -16
- data/lib/screenkit/animation_filters.rb +16 -0
- data/lib/screenkit/callout/styles/base.rb +12 -0
- data/lib/screenkit/callout/styles/file_copy.rb +26 -0
- data/lib/screenkit/callout/styles/inline_block.rb +6 -9
- data/lib/screenkit/callout/styles/{default.rb → shadow_block.rb} +9 -11
- data/lib/screenkit/callout.rb +1 -1
- data/lib/screenkit/cli/episode.rb +1 -1
- data/lib/screenkit/config/episode.rb +6 -0
- data/lib/screenkit/config/project.rb +5 -2
- data/lib/screenkit/duration.rb +5 -0
- data/lib/screenkit/exporter/demotape.rb +20 -2
- data/lib/screenkit/exporter/episode.rb +42 -16
- data/lib/screenkit/exporter/intro.rb +4 -4
- data/lib/screenkit/exporter/outro.rb +5 -5
- data/lib/screenkit/exporter/segment.rb +270 -74
- data/lib/screenkit/generators/episode/callouts/001.yml +12 -0
- data/lib/screenkit/generators/episode/config.yml.erb +8 -8
- data/lib/screenkit/generators/project/screenkit.yml +123 -27
- data/lib/screenkit/schema_validator.rb +1 -1
- data/lib/screenkit/schemas/callout_styles/file_copy.json +8 -0
- data/lib/screenkit/schemas/callout_styles/inline_block.json +20 -0
- data/lib/screenkit/schemas/{callouts/default.json → callout_styles/shadow_block.json} +2 -2
- data/lib/screenkit/schemas/callouts/inline_block.json +20 -11
- data/lib/screenkit/schemas/callouts/shadow_block.json +39 -0
- data/lib/screenkit/schemas/episode.json +7 -55
- data/lib/screenkit/schemas/project.json +6 -6
- data/lib/screenkit/schemas/refs/anchor.json +1 -1
- data/lib/screenkit/schemas/refs/animation.json +1 -1
- data/lib/screenkit/schemas/refs/background.json +1 -1
- data/lib/screenkit/schemas/refs/{callout.json → callout_style.json} +6 -8
- data/lib/screenkit/schemas/refs/color.json +1 -1
- data/lib/screenkit/schemas/refs/demotape.json +48 -0
- data/lib/screenkit/schemas/refs/demotape_themes.json +367 -0
- data/lib/screenkit/schemas/refs/directory.json +1 -1
- data/lib/screenkit/schemas/refs/duration.json +9 -0
- data/lib/screenkit/schemas/refs/intro.json +4 -16
- data/lib/screenkit/schemas/refs/logo.json +1 -1
- data/lib/screenkit/schemas/refs/outro.json +4 -16
- data/lib/screenkit/schemas/refs/position.json +1 -1
- data/lib/screenkit/schemas/refs/scenes.json +2 -6
- data/lib/screenkit/schemas/refs/size.json +1 -1
- data/lib/screenkit/schemas/refs/sound.json +1 -1
- data/lib/screenkit/schemas/refs/spacing.json +1 -1
- data/lib/screenkit/schemas/refs/text_style.json +1 -1
- data/lib/screenkit/schemas/refs/transition.json +2 -6
- data/lib/screenkit/schemas/refs/tts.json +4 -41
- data/lib/screenkit/schemas/refs/tts_builtin.json +23 -0
- data/lib/screenkit/schemas/refs/watermark.json +1 -1
- data/lib/screenkit/schemas/tts/elevenlabs.json +11 -2
- data/lib/screenkit/schemas/tts/espeak.json +26 -0
- data/lib/screenkit/schemas/tts/say.json +12 -2
- data/lib/screenkit/shell.rb +6 -0
- data/lib/screenkit/time_formatter.rb +14 -0
- data/lib/screenkit/tts/base.rb +21 -0
- data/lib/screenkit/tts/eleven_labs.rb +8 -9
- data/lib/screenkit/tts/espeak.rb +30 -0
- data/lib/screenkit/tts/say.rb +5 -6
- data/lib/screenkit/utils.rb +6 -0
- data/lib/screenkit/version.rb +1 -1
- metadata +29 -46
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans-SemiBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Bold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Italic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Light.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Medium.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-Regular.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
- data/lib/screenkit/generators/project/resources/fonts/opensans/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
- /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OFL.txt +0 -0
- /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OpenSans-ExtraBold.ttf +0 -0
- /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/OpenSans-SemiBold.ttf +0 -0
- /data/lib/screenkit/generators/project/resources/fonts/{opensans → open-sans}/README.txt +0 -0
|
@@ -40,7 +40,7 @@ module ScreenKit
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def voiceover_path
|
|
43
|
-
return episode.mute_sound_path unless episode.
|
|
43
|
+
return episode.mute_sound_path unless episode.tts_available?
|
|
44
44
|
|
|
45
45
|
dir = episode.root_dir.join("voiceovers")
|
|
46
46
|
dir.glob("#{prefix}.{#{ContentType.audio.join(',')}}").first ||
|
|
@@ -75,15 +75,20 @@ module ScreenKit
|
|
|
75
75
|
.export(video_path)
|
|
76
76
|
when *ContentType.demotape
|
|
77
77
|
Exporter::Demotape
|
|
78
|
-
.new(
|
|
79
|
-
|
|
78
|
+
.new(
|
|
79
|
+
demotape_path: content_path,
|
|
80
|
+
log_path:,
|
|
81
|
+
options: episode.demotape_options
|
|
82
|
+
).export(video_path)
|
|
80
83
|
else
|
|
81
84
|
raise "Unsupported content type: #{content_path.extname}"
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def crossfade_duration
|
|
86
|
-
|
|
89
|
+
Duration.parse(
|
|
90
|
+
episode.scenes.fetch(:segment).fetch(:crossfade_duration, 0.5)
|
|
91
|
+
)
|
|
87
92
|
end
|
|
88
93
|
|
|
89
94
|
def merge_audio_and_video(log_path:)
|
|
@@ -112,79 +117,19 @@ module ScreenKit
|
|
|
112
117
|
|
|
113
118
|
audio_mix_inputs = ["[1:a]"]
|
|
114
119
|
animation_duration = 0.2
|
|
120
|
+
current_input_index = 2 # Start after video (0) and voiceover (1)
|
|
115
121
|
|
|
116
122
|
callouts.each_with_index do |callout, index|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
in_sound = Sound.new(input: callout_config[:in_transition][:sound],
|
|
120
|
-
source: episode.source)
|
|
121
|
-
out_sound = Sound.new(input: callout_config[:out_transition][:sound],
|
|
122
|
-
source: episode.source)
|
|
123
|
-
|
|
124
|
-
starts_at = callout[:starts_at]
|
|
125
|
-
max_duration = [content_duration - 0.2, 0].max
|
|
126
|
-
duration = callout[:duration]
|
|
127
|
-
.clamp(0, max_duration)
|
|
128
|
-
.round(half: :down)
|
|
129
|
-
ends_at = starts_at + duration
|
|
130
|
-
callout_image_path = episode.output_dir.join("callouts",
|
|
131
|
-
"#{prefix}-#{index}.png")
|
|
132
|
-
image_width, image_height = image_size(callout_image_path)
|
|
133
|
-
|
|
134
|
-
x, y = calculate_position(
|
|
135
|
-
anchor: Anchor.new(callout_config[:anchor]),
|
|
136
|
-
margin: Spacing.new(callout_config[:margin] || 0),
|
|
137
|
-
width: image_width,
|
|
138
|
-
height: image_height
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
inputs += [
|
|
142
|
-
"-loop", "1",
|
|
143
|
-
"-t", duration,
|
|
144
|
-
"-i", callout_image_path,
|
|
145
|
-
|
|
146
|
-
# Add sound for transition in
|
|
147
|
-
"-i", in_sound.path,
|
|
148
|
-
|
|
149
|
-
# Add sound for transition out
|
|
150
|
-
"-i", out_sound.path
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
input_stream = "v#{index}"
|
|
154
|
-
output_stream = "v#{index + 1}"
|
|
155
|
-
callout_index = 2 + (index * 3)
|
|
156
|
-
|
|
157
|
-
animation_filters = AnimationFilters.new(
|
|
158
|
-
content_duration:,
|
|
159
|
-
callout_index:,
|
|
160
|
-
input_stream:,
|
|
161
|
-
output_stream:,
|
|
123
|
+
current_input_index = process_callout(
|
|
124
|
+
callout:,
|
|
162
125
|
index:,
|
|
163
|
-
|
|
164
|
-
ends_at:,
|
|
165
|
-
x:,
|
|
166
|
-
y:,
|
|
126
|
+
content_duration:,
|
|
167
127
|
animation_duration:,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# Delay and mix callout sounds
|
|
175
|
-
in_index = callout_index + 1
|
|
176
|
-
out_index = callout_index + 2
|
|
177
|
-
|
|
178
|
-
filters << "[#{in_index}:a]volume=#{in_sound.volume}," \
|
|
179
|
-
"adelay=#{(starts_at * 1000).to_i}|" \
|
|
180
|
-
"#{(starts_at * 1000).to_i}[in_#{index}]"
|
|
181
|
-
filters << "[#{out_index}:a]volume=#{out_sound.volume}," \
|
|
182
|
-
"adelay=#{(animation_filters[:out_start] * 1000).to_i}|" \
|
|
183
|
-
"#{(animation_filters[:out_start] * 1000).to_i}" \
|
|
184
|
-
"[out_#{index}]"
|
|
185
|
-
|
|
186
|
-
audio_mix_inputs << "[in_#{index}]"
|
|
187
|
-
audio_mix_inputs << "[out_#{index}]"
|
|
128
|
+
inputs:,
|
|
129
|
+
filters:,
|
|
130
|
+
audio_mix_inputs:,
|
|
131
|
+
current_input_index:
|
|
132
|
+
)
|
|
188
133
|
end
|
|
189
134
|
|
|
190
135
|
# Mix all audio streams (voiceover + all sounds)
|
|
@@ -235,7 +180,7 @@ module ScreenKit
|
|
|
235
180
|
def create_voiceover(log_path:)
|
|
236
181
|
return if voiceover_path&.file? && !episode.options.overwrite
|
|
237
182
|
return unless script_path.file?
|
|
238
|
-
return unless episode.
|
|
183
|
+
return unless episode.tts_available?
|
|
239
184
|
|
|
240
185
|
FileUtils.mkdir_p(voiceover_path.dirname)
|
|
241
186
|
|
|
@@ -253,6 +198,257 @@ module ScreenKit
|
|
|
253
198
|
|
|
254
199
|
format(path.to_s, prefix:) if path
|
|
255
200
|
end
|
|
201
|
+
|
|
202
|
+
def process_callout(
|
|
203
|
+
callout:,
|
|
204
|
+
index:,
|
|
205
|
+
content_duration:,
|
|
206
|
+
animation_duration:,
|
|
207
|
+
inputs:,
|
|
208
|
+
filters:,
|
|
209
|
+
audio_mix_inputs:,
|
|
210
|
+
current_input_index:
|
|
211
|
+
)
|
|
212
|
+
type = callout[:type].to_sym
|
|
213
|
+
callout_config = episode.callout_styles[type]
|
|
214
|
+
in_sound = Sound.new(input: callout_config[:in_transition][:sound],
|
|
215
|
+
source: episode.source)
|
|
216
|
+
out_sound = Sound.new(input: callout_config[:out_transition][:sound],
|
|
217
|
+
source: episode.source)
|
|
218
|
+
|
|
219
|
+
starts_at = TimeFormatter.parse(callout[:starts_at])
|
|
220
|
+
max_duration = [content_duration - 0.2, 0].max
|
|
221
|
+
duration = Duration.parse(callout[:duration])
|
|
222
|
+
.clamp(0, max_duration)
|
|
223
|
+
.round(half: :down)
|
|
224
|
+
ends_at = starts_at + duration
|
|
225
|
+
callout_duration = ends_at - starts_at
|
|
226
|
+
|
|
227
|
+
callout_path = find_callout_path(index)
|
|
228
|
+
video_callout = video_callout?(callout_path)
|
|
229
|
+
has_video_audio = has_audio?(callout_path)
|
|
230
|
+
|
|
231
|
+
callout_width, callout_height = image_size(callout_path)
|
|
232
|
+
x, y = calculate_callout_position(
|
|
233
|
+
video_callout:,
|
|
234
|
+
callout_config:,
|
|
235
|
+
callout_width:,
|
|
236
|
+
callout_height:
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
callout_index, current_input_index = add_callout_inputs(
|
|
240
|
+
inputs:,
|
|
241
|
+
current_input_index:,
|
|
242
|
+
callout_path:,
|
|
243
|
+
video_callout:,
|
|
244
|
+
has_video_audio:,
|
|
245
|
+
duration:
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
in_sound_index, out_sound_index, current_input_index =
|
|
249
|
+
add_transition_sound_inputs(
|
|
250
|
+
inputs:,
|
|
251
|
+
current_input_index:,
|
|
252
|
+
video_callout:,
|
|
253
|
+
in_sound:,
|
|
254
|
+
out_sound:
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
animation_filters = add_video_filters(
|
|
258
|
+
filters:,
|
|
259
|
+
index:,
|
|
260
|
+
callout_index:,
|
|
261
|
+
callout_config:,
|
|
262
|
+
video_callout:,
|
|
263
|
+
content_duration:,
|
|
264
|
+
starts_at:,
|
|
265
|
+
ends_at:,
|
|
266
|
+
x:,
|
|
267
|
+
y:,
|
|
268
|
+
animation_duration:,
|
|
269
|
+
callout_width:,
|
|
270
|
+
callout_height:
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
add_audio_filters(
|
|
274
|
+
filters:,
|
|
275
|
+
audio_mix_inputs:,
|
|
276
|
+
index:,
|
|
277
|
+
callout_index:,
|
|
278
|
+
video_callout:,
|
|
279
|
+
has_video_audio:,
|
|
280
|
+
starts_at:,
|
|
281
|
+
callout_duration:,
|
|
282
|
+
in_sound:,
|
|
283
|
+
out_sound:,
|
|
284
|
+
in_sound_index:,
|
|
285
|
+
out_sound_index:,
|
|
286
|
+
animation_filters:
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
current_input_index
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def find_callout_path(index)
|
|
293
|
+
callout_path = episode.output_dir.join("callouts").glob(
|
|
294
|
+
"#{prefix}-#{index}.{png,#{ContentType.video.join(',')}}"
|
|
295
|
+
).first
|
|
296
|
+
|
|
297
|
+
raise "Callout file not found for #{prefix}-#{index}" unless callout_path
|
|
298
|
+
|
|
299
|
+
callout_path
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def video_callout?(callout_path)
|
|
303
|
+
ContentType.video.include?(callout_path.extname.delete_prefix("."))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def calculate_callout_position(
|
|
307
|
+
video_callout:,
|
|
308
|
+
callout_config:,
|
|
309
|
+
callout_width:,
|
|
310
|
+
callout_height:
|
|
311
|
+
)
|
|
312
|
+
# For video callouts, ignore anchor/margin and position at 0,0
|
|
313
|
+
# (assumes videos are already properly sized and positioned)
|
|
314
|
+
if video_callout
|
|
315
|
+
[0, 0]
|
|
316
|
+
else
|
|
317
|
+
calculate_position(
|
|
318
|
+
anchor: Anchor.new(callout_config[:anchor]),
|
|
319
|
+
margin: Spacing.new(callout_config[:margin] || 0),
|
|
320
|
+
width: callout_width,
|
|
321
|
+
height: callout_height
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def add_callout_inputs(
|
|
327
|
+
inputs:,
|
|
328
|
+
current_input_index:,
|
|
329
|
+
callout_path:,
|
|
330
|
+
video_callout:,
|
|
331
|
+
has_video_audio:,
|
|
332
|
+
duration:
|
|
333
|
+
)
|
|
334
|
+
callout_index = current_input_index
|
|
335
|
+
|
|
336
|
+
if video_callout
|
|
337
|
+
# Don't use -t for videos, let them play naturally
|
|
338
|
+
inputs << "-i" << callout_path
|
|
339
|
+
current_input_index += 1
|
|
340
|
+
|
|
341
|
+
# Add mute audio if video has no audio
|
|
342
|
+
unless has_video_audio
|
|
343
|
+
inputs << "-t" << duration << "-i" << episode.mute_sound_path
|
|
344
|
+
current_input_index += 1
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
inputs << "-loop" << "1" << "-t" << duration << "-i" << callout_path
|
|
348
|
+
current_input_index += 1
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
[callout_index, current_input_index]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def add_transition_sound_inputs(
|
|
355
|
+
inputs:,
|
|
356
|
+
current_input_index:,
|
|
357
|
+
video_callout:,
|
|
358
|
+
in_sound:,
|
|
359
|
+
out_sound:
|
|
360
|
+
)
|
|
361
|
+
# Add transition sounds (only for non-video callouts)
|
|
362
|
+
return [nil, nil, current_input_index] if video_callout
|
|
363
|
+
|
|
364
|
+
in_sound_index = current_input_index
|
|
365
|
+
inputs << "-i" << in_sound.path
|
|
366
|
+
current_input_index += 1
|
|
367
|
+
|
|
368
|
+
out_sound_index = current_input_index
|
|
369
|
+
inputs << "-i" << out_sound.path
|
|
370
|
+
current_input_index += 1
|
|
371
|
+
|
|
372
|
+
[in_sound_index, out_sound_index, current_input_index]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def add_video_filters(
|
|
376
|
+
filters:,
|
|
377
|
+
index:,
|
|
378
|
+
callout_index:,
|
|
379
|
+
callout_config:,
|
|
380
|
+
video_callout:,
|
|
381
|
+
content_duration:,
|
|
382
|
+
starts_at:,
|
|
383
|
+
ends_at:,
|
|
384
|
+
x:,
|
|
385
|
+
y:,
|
|
386
|
+
animation_duration:,
|
|
387
|
+
callout_width:,
|
|
388
|
+
callout_height:
|
|
389
|
+
)
|
|
390
|
+
input_stream = "v#{index}"
|
|
391
|
+
output_stream = "v#{index + 1}"
|
|
392
|
+
animation_method = video_callout ? :video : callout_config[:animation]
|
|
393
|
+
|
|
394
|
+
animation_filters = AnimationFilters.new(
|
|
395
|
+
content_duration:,
|
|
396
|
+
callout_index:,
|
|
397
|
+
input_stream:,
|
|
398
|
+
output_stream:,
|
|
399
|
+
index:,
|
|
400
|
+
starts_at:,
|
|
401
|
+
ends_at:,
|
|
402
|
+
x:,
|
|
403
|
+
y:,
|
|
404
|
+
animation_duration:,
|
|
405
|
+
image_width: callout_width,
|
|
406
|
+
image_height: callout_height
|
|
407
|
+
).send(animation_method)
|
|
408
|
+
|
|
409
|
+
filters.concat(animation_filters[:video])
|
|
410
|
+
|
|
411
|
+
animation_filters
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def add_audio_filters(
|
|
415
|
+
filters:,
|
|
416
|
+
audio_mix_inputs:,
|
|
417
|
+
index:,
|
|
418
|
+
callout_index:,
|
|
419
|
+
video_callout:,
|
|
420
|
+
has_video_audio:,
|
|
421
|
+
starts_at:,
|
|
422
|
+
callout_duration:,
|
|
423
|
+
in_sound:,
|
|
424
|
+
out_sound:,
|
|
425
|
+
in_sound_index:,
|
|
426
|
+
out_sound_index:,
|
|
427
|
+
animation_filters:
|
|
428
|
+
)
|
|
429
|
+
# Mix video audio if present
|
|
430
|
+
if video_callout && has_video_audio
|
|
431
|
+
filters <<
|
|
432
|
+
"[#{callout_index}:a]atrim=end=#{callout_duration}," \
|
|
433
|
+
"asetpts=PTS-STARTPTS,adelay=#{(starts_at * 1000).to_i}|" \
|
|
434
|
+
"#{(starts_at * 1000).to_i}[video_audio_#{index}]"
|
|
435
|
+
audio_mix_inputs << "[video_audio_#{index}]"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# For non-video callouts, add transition sounds
|
|
439
|
+
return if video_callout
|
|
440
|
+
|
|
441
|
+
filters << "[#{in_sound_index}:a]volume=#{in_sound.volume}," \
|
|
442
|
+
"adelay=#{(starts_at * 1000).to_i}|" \
|
|
443
|
+
"#{(starts_at * 1000).to_i}[in_#{index}]"
|
|
444
|
+
filters << "[#{out_sound_index}:a]volume=#{out_sound.volume}," \
|
|
445
|
+
"adelay=#{(animation_filters[:out_start] * 1000).to_i}|" \
|
|
446
|
+
"#{(animation_filters[:out_start] * 1000).to_i}" \
|
|
447
|
+
"[out_#{index}]"
|
|
448
|
+
|
|
449
|
+
audio_mix_inputs << "[in_#{index}]"
|
|
450
|
+
audio_mix_inputs << "[out_#{index}]"
|
|
451
|
+
end
|
|
256
452
|
end
|
|
257
453
|
end
|
|
258
454
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
# yaml-language-server: $schema
|
|
2
|
+
# yaml-language-server: $schema=https://screenkit.dev/schemas/episode.json
|
|
3
3
|
|
|
4
4
|
# The episode title. When there are no linebreaks, they will be inferred based
|
|
5
5
|
# on the approximate text width.
|
|
@@ -10,7 +10,7 @@ title: "CREATING SCREENCASTS WITH SCREENKIT"
|
|
|
10
10
|
#
|
|
11
11
|
# The project's configuration can define the callout structure, while the
|
|
12
12
|
# episode configuration can define the content and timing.
|
|
13
|
-
|
|
13
|
+
callouts_styles:
|
|
14
14
|
- type: info
|
|
15
15
|
title: "ScreenKit"
|
|
16
16
|
body: "Visit https://github.com/fnando/screenkit to learn more."
|
|
@@ -45,7 +45,7 @@ callouts:
|
|
|
45
45
|
# title:
|
|
46
46
|
# x: 100
|
|
47
47
|
# y: 300
|
|
48
|
-
# font_path:
|
|
48
|
+
# font_path: open-sans/OpenSans-ExtraBold.ttf
|
|
49
49
|
# size: 144
|
|
50
50
|
# color: "#ffffff"
|
|
51
51
|
#
|
|
@@ -53,7 +53,7 @@ callouts:
|
|
|
53
53
|
# # Works best with a transparent PNG file in high resolution (at least 2x the
|
|
54
54
|
# # size it'll be displayed).
|
|
55
55
|
# logo:
|
|
56
|
-
# path:
|
|
56
|
+
# path: images/logo.png
|
|
57
57
|
# x: 100
|
|
58
58
|
# y: 200
|
|
59
59
|
# width: 200
|
|
@@ -62,7 +62,7 @@ callouts:
|
|
|
62
62
|
# # The sound to be played along with the logo in the intro.
|
|
63
63
|
# # This must correspond to a sound file in the sounds directory, with any
|
|
64
64
|
# # audio extension (e.g. mp3, m4a, wav).
|
|
65
|
-
# sound:
|
|
65
|
+
# sound: sounds/chime.mp3
|
|
66
66
|
#
|
|
67
67
|
# outro:
|
|
68
68
|
# # The duration of the outro scene in seconds.
|
|
@@ -78,11 +78,11 @@ callouts:
|
|
|
78
78
|
# background: "#100f50"
|
|
79
79
|
#
|
|
80
80
|
# # The sound to be played along with the logo in the outro.
|
|
81
|
-
# sound:
|
|
81
|
+
# sound: sounds/chime.mp3
|
|
82
82
|
#
|
|
83
83
|
# # The logo to be displayed in the video outro.
|
|
84
84
|
# logo:
|
|
85
|
-
# path:
|
|
85
|
+
# path: images/logo.png
|
|
86
86
|
# x: center
|
|
87
87
|
# y: center
|
|
88
88
|
# width: 500
|
|
@@ -95,7 +95,7 @@ callouts:
|
|
|
95
95
|
# When present, this will have higher precedence over the project's
|
|
96
96
|
# configuration, so you can have different intro/outro segments for each
|
|
97
97
|
# episode.
|
|
98
|
-
#
|
|
98
|
+
# tts:
|
|
99
99
|
# engine: elevenlabs
|
|
100
100
|
# voice_id: 56AoDkrOh6qfVPDXZ7Pt
|
|
101
101
|
# language_code: en
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# yaml-language-server: $schema=../../schemas/project.json
|
|
3
|
+
## yaml-language-server: $schema=https://screenkit.dev/schemas/project.json
|
|
3
4
|
|
|
4
5
|
# ScreenKit project configuration file.
|
|
5
6
|
schema: 1
|
|
@@ -76,19 +77,35 @@ watermark:
|
|
|
76
77
|
margin: 100
|
|
77
78
|
|
|
78
79
|
# TTS configuration.
|
|
79
|
-
# To disable voiceover entirely, set `tts: false
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
# stability: 0.5
|
|
87
|
-
# similarity: 0.75
|
|
88
|
-
# style: 0.0
|
|
80
|
+
# To disable voiceover entirely, set `tts: false` or disable all engines
|
|
81
|
+
# individually.
|
|
82
|
+
#
|
|
83
|
+
# You can set up multiple TTS engines to be used as fallbacks in case one fails.
|
|
84
|
+
# Each TTS engine has its own detection mechanism. For instance, say and espeak
|
|
85
|
+
# checks for a binary with the same name. ElevenLabs checks for the presence of
|
|
86
|
+
# `--tts-api-key`.
|
|
89
87
|
tts:
|
|
90
|
-
engine
|
|
91
|
-
|
|
88
|
+
# Apple Say TTS engine configuration.
|
|
89
|
+
- engine: say
|
|
90
|
+
rate: 150
|
|
91
|
+
enabled: true
|
|
92
|
+
|
|
93
|
+
# eSpeak TTS engine configuration.
|
|
94
|
+
- engine: espeak
|
|
95
|
+
rate: 150
|
|
96
|
+
voice: en-us
|
|
97
|
+
enabled: true
|
|
98
|
+
|
|
99
|
+
# Eleven Labs TTS engine configuration.
|
|
100
|
+
- engine: eleven_labs
|
|
101
|
+
enabled: true
|
|
102
|
+
voice_id: 56AoDkrOh6qfVPDXZ7Pt
|
|
103
|
+
language_code: en
|
|
104
|
+
voice_settings:
|
|
105
|
+
speed: 0.9
|
|
106
|
+
stability: 0.5
|
|
107
|
+
similarity: 0.75
|
|
108
|
+
style: 0.0
|
|
92
109
|
|
|
93
110
|
# Scene configurations define how different scenes in the video are rendered.
|
|
94
111
|
# There are three types of scenes: intro, outro, and segment.
|
|
@@ -98,13 +115,13 @@ tts:
|
|
|
98
115
|
scenes:
|
|
99
116
|
intro:
|
|
100
117
|
# The duration of the intro scene in seconds.
|
|
101
|
-
duration: 5.
|
|
118
|
+
duration: 5.5s
|
|
102
119
|
|
|
103
120
|
# The fade-in duration in seconds.
|
|
104
|
-
fade_in:
|
|
121
|
+
fade_in: 0s
|
|
105
122
|
|
|
106
123
|
# The fade-out duration in seconds.
|
|
107
|
-
fade_out: 0.
|
|
124
|
+
fade_out: 0.5s
|
|
108
125
|
|
|
109
126
|
# The background color/image of the intro scene.
|
|
110
127
|
background: "#100f50"
|
|
@@ -113,7 +130,7 @@ scenes:
|
|
|
113
130
|
title:
|
|
114
131
|
x: 100
|
|
115
132
|
y: 300
|
|
116
|
-
font_path:
|
|
133
|
+
font_path: open-sans/OpenSans-ExtraBold.ttf
|
|
117
134
|
size: 144
|
|
118
135
|
color: "#ffffff"
|
|
119
136
|
|
|
@@ -133,13 +150,13 @@ scenes:
|
|
|
133
150
|
|
|
134
151
|
outro:
|
|
135
152
|
# The duration of the outro scene in seconds.
|
|
136
|
-
duration: 5.
|
|
153
|
+
duration: 5.5s
|
|
137
154
|
|
|
138
155
|
# The fade-in duration in seconds.
|
|
139
|
-
fade_in: 0.
|
|
156
|
+
fade_in: 0.5s
|
|
140
157
|
|
|
141
158
|
# The fade-out duration in seconds.
|
|
142
|
-
fade_out: 0.
|
|
159
|
+
fade_out: 0.5s
|
|
143
160
|
|
|
144
161
|
# The background color/image of the outro scene.
|
|
145
162
|
background: "#100f50"
|
|
@@ -156,34 +173,113 @@ scenes:
|
|
|
156
173
|
|
|
157
174
|
segment:
|
|
158
175
|
# The duration of the crossover transition between segments in seconds.
|
|
159
|
-
crossfade_duration: 0.
|
|
176
|
+
crossfade_duration: 0.5s
|
|
177
|
+
|
|
178
|
+
# Configure [demotape](https://github.com/fnando/demotape).
|
|
179
|
+
demotape:
|
|
180
|
+
# Global speed for typing
|
|
181
|
+
typing_speed: 50ms
|
|
182
|
+
|
|
183
|
+
# Whether to add variability to typing speed
|
|
184
|
+
variable_typing: 0.25
|
|
185
|
+
|
|
186
|
+
# Shell to use for the terminal session
|
|
187
|
+
shell: zsh
|
|
188
|
+
|
|
189
|
+
# Path to a shell rc file to source on startup
|
|
190
|
+
# rc_file:
|
|
191
|
+
|
|
192
|
+
# Do not load the default rc file
|
|
193
|
+
no_rc: false
|
|
194
|
+
|
|
195
|
+
# A built-in theme name or a path to a custom theme JSON file
|
|
196
|
+
theme: default_dark
|
|
197
|
+
|
|
198
|
+
# Font size for the terminal session
|
|
199
|
+
font_size: 32
|
|
200
|
+
|
|
201
|
+
# Font family for the terminal session
|
|
202
|
+
font_family: "'JetBrainsMono Nerd Font Propo', monospace"
|
|
203
|
+
|
|
204
|
+
# Line height for the terminal session
|
|
205
|
+
line_height: 1.2
|
|
206
|
+
|
|
207
|
+
# Whether the cursor blinks
|
|
208
|
+
cursor_blink: true
|
|
209
|
+
|
|
210
|
+
# The width of the cursor when in 'bar' style
|
|
211
|
+
cursor_width: 2
|
|
212
|
+
|
|
213
|
+
# The style of the cursor when the terminal is focused.
|
|
214
|
+
cursor_style: block
|
|
215
|
+
|
|
216
|
+
# Letter spacing for the terminal session
|
|
217
|
+
letter_spacing: 0
|
|
218
|
+
|
|
219
|
+
# Padding around the terminal content. It can be 1, 2, or 4 numbers.
|
|
220
|
+
padding: 50
|
|
221
|
+
|
|
222
|
+
# Loop the GIF infinitely
|
|
223
|
+
loop: true
|
|
224
|
+
|
|
225
|
+
# Delay time (in seconds) before restarting the GIF animation
|
|
226
|
+
loop_delay: 0
|
|
227
|
+
|
|
228
|
+
# Margin around the terminal content. It can be 1, 2, or 4 numbers.
|
|
229
|
+
margin: 0
|
|
230
|
+
|
|
231
|
+
# Color or image to use for the margin around the terminal content
|
|
232
|
+
margin_fill: "#000000"
|
|
233
|
+
|
|
234
|
+
# Border radius for the terminal canvas
|
|
235
|
+
border_radius: 0
|
|
236
|
+
|
|
237
|
+
# Delay (in seconds) before pressing Enter
|
|
238
|
+
run_enter_delay: 1s
|
|
239
|
+
|
|
240
|
+
# Delay (in seconds) after running each command
|
|
241
|
+
run_sleep: 3s
|
|
160
242
|
|
|
161
243
|
# Callout configurations define the appearance and behavior of callouts used
|
|
162
244
|
# in the video. Callouts are visual elements that highlight important
|
|
163
245
|
# information. Each callout type can have its own settings for icon, background,
|
|
164
246
|
# text styles, animations, and sounds.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# yaml-language-server: $schema
|
|
247
|
+
callout_styles:
|
|
248
|
+
shadow_block:
|
|
249
|
+
# yaml-language-server: $schema=https://screenkit.dev/schemas/callout_styles/default.json
|
|
250
|
+
style: shadow_block
|
|
168
251
|
background_color: "#ffff00"
|
|
169
252
|
shadow: "#2242d3"
|
|
170
253
|
title_style:
|
|
171
254
|
color: "#000000"
|
|
172
255
|
size: 40
|
|
173
|
-
font_path:
|
|
256
|
+
font_path: open-sans/OpenSans-ExtraBold.ttf
|
|
174
257
|
body_style:
|
|
175
258
|
color: "#000000"
|
|
176
259
|
size: 32
|
|
177
|
-
font_path:
|
|
260
|
+
font_path: open-sans/OpenSans-Semibold.ttf
|
|
178
261
|
margin: 100
|
|
179
262
|
padding: 50
|
|
180
263
|
anchor: [left, bottom]
|
|
181
264
|
animation: fade
|
|
182
265
|
in_transition:
|
|
183
|
-
duration: 0.
|
|
266
|
+
duration: 0.4s
|
|
184
267
|
sound: pop.mp3
|
|
185
268
|
out_transition:
|
|
186
|
-
duration: 0.
|
|
269
|
+
duration: 0.3s
|
|
187
270
|
sound:
|
|
188
271
|
path: pop.mp3
|
|
189
272
|
volume: 0.7
|
|
273
|
+
|
|
274
|
+
inline_block:
|
|
275
|
+
# yaml-language-server: $schema=https://screenkit.dev/schemas/callout_styles/inline_block.json
|
|
276
|
+
style: inline_block
|
|
277
|
+
anchor: [left, bottom]
|
|
278
|
+
margin: 100
|
|
279
|
+
animation: fade
|
|
280
|
+
in_transition:
|
|
281
|
+
duration: 0.4s
|
|
282
|
+
sound: pop.mp3
|
|
283
|
+
out_transition:
|
|
284
|
+
duration: 0.4s
|
|
285
|
+
sound: pop.mp3
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
3
|
+
"$id": "https://screenkit.dev/schemas/callout_styles/file_copy.json",
|
|
4
|
+
"title": "File Copy Callout",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["file_path"],
|
|
7
|
+
"properties": { "file_path": { "type": "string", "format": "uri" } }
|
|
8
|
+
}
|