screenkit 0.0.6 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97a973d1c6ae7d7c373be709a2233a0b935fae81c706f312c03664e730088780
4
- data.tar.gz: 2bf3c1093ee252e020635ef4f98632975eab1c77436e0769d9b2a3c107cd228c
3
+ metadata.gz: 3dc4a6ebfa5fb6725aa40e4bbe699bcafca860135ef8cca01fc48ab66731392f
4
+ data.tar.gz: 42df45d24c11f800e20fff4dfc030102bef5747f758ee47378d98a56dd8732f8
5
5
  SHA512:
6
- metadata.gz: 78c95717248ebfa4fab374c612656813bdf6cd0add77dd1c4dfdc65505033c793b73c1645daeb5742670dfc0e6e5aef84100fabd5ec8cc15a011e95d88fa62e0
7
- data.tar.gz: 1cca22fd0d8f922fc245be632fcbf587968dc99c402615fe8691eb1e1c45415583f8c1fd2f00c9898c3855a14fd6efe3803c3601102fe8965d563288b5acb4c8
6
+ metadata.gz: c0eac47f990100559123217b0a54f1ae84629ea6ff7039c38d6212d72284cce07b10552f92b6504588ab74a013bb3242d8bc19fe66a7a06b0cb3a8014822248b
7
+ data.tar.gz: 5c43aae473d552c8f0d233c0ab19b8ca1a1eab91b26225b7533c53eb4caeb44e5909cbc5e20c04b66bad939acee49219d0a75f61edb1617e00e209e498546567
@@ -7,7 +7,7 @@ on:
7
7
  paths:
8
8
  - Dockerfile
9
9
  tags:
10
- - v*
10
+ - v[0-9]+.[0-9]+.[0-9]+
11
11
  workflow_dispatch:
12
12
  inputs:
13
13
  ref:
@@ -23,7 +23,7 @@ jobs:
23
23
  build:
24
24
  runs-on: ubuntu-latest
25
25
  steps:
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v6
27
27
  with:
28
28
  ref: ${{ github.event.inputs.ref }}
29
29
 
data/CHANGELOG.md CHANGED
@@ -11,6 +11,10 @@ Prefix your message with one of the following:
11
11
  - [Security] in case of vulnerabilities.
12
12
  -->
13
13
 
14
+ ## v0.0.7
15
+
16
+ - [Fixed] Improve support for extensions.
17
+
14
18
  ## v0.0.6
15
19
 
16
20
  - [Added] Add `--tts-preset` option to select TTS preset when exporting
data/DOCUMENTATION.md CHANGED
@@ -764,7 +764,7 @@ module. Custom engines must implement the `generate` method:
764
764
  module ScreenKit
765
765
  module TTS
766
766
  class CustomEngine < Base
767
- include Shell
767
+ extend Shell
768
768
 
769
769
  # Optional: Define schema path for validation
770
770
  def self.schema_path
@@ -772,8 +772,8 @@ module ScreenKit
772
772
  end
773
773
 
774
774
  # This method is required.
775
- def available?
776
- enabled? && command_exist?("some-command")
775
+ def self.available?(**)
776
+ command_exist?("some-command")
777
777
  end
778
778
 
779
779
  # This method is required.
@@ -781,15 +781,20 @@ module ScreenKit
781
781
  # Optional: validate options against JSON schema.
782
782
  self.class.validate!(options)
783
783
 
784
+ # If you need access to previous/next text, you can access the method
785
+ # `segments`.
786
+ # current_index = segments.index { it.script_content == text }
787
+ # next_text = segments[current_index + 1]&.script_content
788
+
784
789
  # Generate audio file from text
785
790
  # Write output to output_path
786
791
  # Optionally log to log_path
787
792
 
788
793
  # Example implementation:
789
- # run_command "some-command",
790
- # "-o", output_path.sub_ext(".wav"),
791
- # text,
792
- # log_path:
794
+ # self.class.run_command "some-command",
795
+ # "-o", output_path.sub_ext(".wav"),
796
+ # text,
797
+ # log_path:
793
798
  end
794
799
  end
795
800
  end
@@ -802,6 +807,7 @@ end
802
807
  tts:
803
808
  - id: custom_engine
804
809
  engine: custom_engine # Camelized to CustomEngine
810
+ enabled: true
805
811
  # Add your custom options here
806
812
  api_key: your_api_key
807
813
  custom_option: value
@@ -811,6 +817,16 @@ The engine name is camelized (e.g., `custom_engine` → `CustomEngine`,
811
817
  `google_cloud` → `GoogleCloud`) and loaded as
812
818
  `ScreenKit::TTS::#{CamelizedName}`.
813
819
 
820
+ ### 3rd-party TTS Engines
821
+
822
+ - [Search Github](https://github.com/topics/screenkit-tts)
823
+ - [Google Text to Speech](https://github.com/fnando/screenkit-tts-google)
824
+
825
+ > [!TIP]
826
+ >
827
+ > If you host your TTS engine on Github, use the topic `screekit-tts`, so other
828
+ > people can find it.
829
+
814
830
  ---
815
831
 
816
832
  ## Animations
@@ -897,6 +913,12 @@ Enter
897
913
  Sleep 2s
898
914
  ```
899
915
 
916
+ When running tape files, the working directory will be the episode directory. If
917
+ you're importing anything from the parent directory, you must specify relative
918
+ paths accordingly. For instance, `episodes/001-episode-name/content/001.tape`
919
+ would need to reference `../../resources/some-file` to access
920
+ `resources/some-file` in the project's directory.
921
+
900
922
  ### Script Files
901
923
 
902
924
  Plain text files for voiceover generation:
data/action.yml ADDED
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: ScreenKit Episode
3
+ description: Export a screencast episode using Docker
4
+ inputs:
5
+ episode:
6
+ description: "Episode number (e.g. 001)"
7
+ required: true
8
+ tts_preset:
9
+ description: "TTS preset name"
10
+ default: ""
11
+ required: false
12
+ tts_api_key:
13
+ description: "TTS API key"
14
+ required: false
15
+ overwrite:
16
+ description: "Regenerate files"
17
+ default: "false"
18
+ required: false
19
+ match:
20
+ description: "Match pattern for segments (e.g. 001)"
21
+ default: ""
22
+ required: false
23
+ github_token:
24
+ description: "GitHub token for cache operations"
25
+ required: true
26
+ retention:
27
+ description: "Retention days for artifacts"
28
+ default: "2"
29
+
30
+ runs:
31
+ using: composite
32
+ steps:
33
+ - name: Generate cache keys
34
+ shell: bash
35
+ run: |
36
+ VOICEOVER_HASH=$(find episodes/${{ inputs.episode }}-*/scripts/*.txt -type f 2>/dev/null | sort | xargs sha256sum 2>/dev/null | sha256sum | cut -d' ' -f1 || echo "none")
37
+ CONTENT_HASH=$(find episodes/${{ inputs.episode }}-*/*/*.* -type f 2>/dev/null | sort | xargs sha256sum 2>/dev/null | sha256sum | cut -d' ' -f1 || echo "none")
38
+
39
+ echo "VOICEOVER_CACHE_KEY=episode-voiceover-${{ inputs.episode }}-${VOICEOVER_HASH}" >> $GITHUB_ENV
40
+ echo "VOICEOVER_RESTORE_KEY=episode-voiceover-${{ inputs.episode }}-" >> $GITHUB_ENV
41
+ echo "VIDEO_CACHE_KEY=episode-${{ inputs.episode }}-${CONTENT_HASH}" >> $GITHUB_ENV
42
+ echo "VIDEO_RESTORE_KEY=episode-${{ inputs.episode }}-" >> $GITHUB_ENV
43
+
44
+ - name: Find episode dir
45
+ shell: bash
46
+ run: |
47
+ EPISODE_DIR=$(ls -1d episodes/${{ inputs.episode }}-* | head -n 1)
48
+ echo "EPISODE_DIR=$EPISODE_DIR" >> $GITHUB_ENV
49
+
50
+ if [[ ! -d "$EPISODE_DIR" ]]; then
51
+ echo "Episode directory not found!"
52
+ echo "Available episodes:"
53
+ ls -1 episodes
54
+ exit 1
55
+ fi
56
+
57
+ - name: Restore episode voice cache
58
+ id: cache-voice
59
+ uses: actions/cache/restore@v4
60
+ with:
61
+ path: episodes/${{ inputs.episode }}-*/voiceovers
62
+ key: ${{ env.VOICEOVER_CACHE_KEY }}
63
+ restore-keys: ${{ env.VOICEOVER_RESTORE_KEY }}
64
+ enableCrossOsArchive: true
65
+
66
+ - name: Restore videos cache
67
+ id: cache-videos
68
+ uses: actions/cache/restore@v4
69
+ with:
70
+ path: output/${{ inputs.episode }}-*/videos/*.mp4
71
+ key: ${{ env.VIDEO_CACHE_KEY }}
72
+ restore-keys: ${{ env.VIDEO_RESTORE_KEY }}
73
+ enableCrossOsArchive: true
74
+
75
+ - name: Export Episode
76
+ shell: bash
77
+ id: screenkit
78
+ run: |
79
+ set -e
80
+ docker run \
81
+ --rm \
82
+ --cap-add=SYS_ADMIN \
83
+ --shm-size=2g \
84
+ --security-opt seccomp=unconfined \
85
+ -v ${{ github.workspace }}:/source \
86
+ fnando/screenkit:latest \
87
+ episode export \
88
+ --dir="${{ env.EPISODE_DIR }}" \
89
+ ${{ inputs.match != '' && format('--match {0}', inputs.match) || '' }} \
90
+ ${{ inputs.overwrite == 'true' && '--overwrite' || '--no-overwrite' }} \
91
+ ${{ inputs.tts_preset != '' && format('--tts-preset {0}', inputs.tts_preset) || '' }} \
92
+ ${{ inputs.tts_api_key != '' && format('--tts-api-key {0}', inputs.tts_api_key) || '' }}
93
+ echo "result=true" >> $GITHUB_OUTPUT
94
+
95
+ - name: Delete voice cache
96
+ if: steps.screenkit.outputs.result == 'true'
97
+ shell: bash
98
+ run: gh cache delete "${{ env.VOICEOVER_CACHE_KEY }}" || true
99
+ env:
100
+ GH_TOKEN: ${{ inputs.github_token }}
101
+
102
+ - name: Delete videos cache
103
+ if: steps.screenkit.outputs.result == 'true'
104
+ shell: bash
105
+ run: gh cache delete "${{ env.VIDEO_CACHE_KEY }}" || true
106
+ env:
107
+ GH_TOKEN: ${{ inputs.github_token }}
108
+
109
+ - name: List cache entries
110
+ shell: bash
111
+ run: |
112
+ echo "= Voiceover Cache ="
113
+ ls -la episodes/${{ inputs.episode }}-*/voiceovers || true
114
+ echo
115
+ echo "= Videos Cache ="
116
+ ls -la output/${{ inputs.episode }}-*/videos || true
117
+
118
+ - name: Save episode voiceover cache
119
+ if: steps.screenkit.outputs.result == 'true'
120
+ uses: actions/cache/save@v4
121
+ with:
122
+ path: episodes/${{ inputs.episode }}-*/voiceovers
123
+ key: ${{ env.VOICEOVER_CACHE_KEY }}
124
+ enableCrossOsArchive: true
125
+
126
+ - name: Save video recordings cache
127
+ if: steps.screenkit.outputs.result == 'true'
128
+ uses: actions/cache/save@v4
129
+ with:
130
+ path: output/${{ inputs.episode }}-*/videos/*.mp4
131
+ key: ${{ env.VIDEO_CACHE_KEY }}
132
+ enableCrossOsArchive: true
133
+
134
+ - name: Upload output artifacts
135
+ uses: actions/upload-artifact@v5
136
+ if: always()
137
+ with:
138
+ name: output
139
+ path: output
140
+ retention-days: ${{ inputs.retention }}
@@ -4,6 +4,8 @@ module ScreenKit
4
4
  class Callout
5
5
  module Styles
6
6
  class Base
7
+ require_relative "../../schema_validator"
8
+
7
9
  attr_reader :source, :output_path, :log_path
8
10
  attr_accessor :options
9
11
 
@@ -16,6 +18,11 @@ module ScreenKit
16
18
  @options = options
17
19
  end
18
20
 
21
+ # Wrap text to fit within the specified maximum width.
22
+ # @param text [String] The text to wrap.
23
+ # @param max_width [Integer] The maximum width in pixels.
24
+ # @param font_size [Integer] The font size in points.
25
+ # @return [Array<String>] The wrapped lines of text.
19
26
  def text_wrap(text, max_width:, font_size:)
20
27
  words = text.to_s.split(/\s+/)
21
28
  width_factor = 0.6
@@ -42,10 +49,19 @@ module ScreenKit
42
49
  text.gsub("'", "\\\\'")
43
50
  end
44
51
 
52
+ # Remove a file if it exists.
53
+ # @param path [String] The file path to remove.
45
54
  def remove_file(path)
46
55
  File.unlink(path) if path && File.exist?(path)
47
56
  end
48
57
 
58
+ # Render text into an image using MiniMagick.
59
+ # @param text [String] The text to render.
60
+ # @param style [TextStyle] The text style to apply.
61
+ # @param width [Integer] The width of the text image.
62
+ # @param type [String] The ImageMagick text type (e.g., "caption").
63
+ # @return [Array] The path to the generated text image, width, and
64
+ # height.
49
65
  def render_text_image(text:, style:, width:, type:)
50
66
  return [nil, 0, 0] if text.to_s.empty?
51
67
 
@@ -61,9 +61,13 @@ module ScreenKit
61
61
  def export
62
62
  puts Banner.banner if options.banner
63
63
 
64
- episode_config = Config::Episode.load_file(
65
- File.join(options.dir, "config.yml")
66
- )
64
+ episode_config = File.join(options.dir, "config.yml")
65
+
66
+ episode_config = if File.file?(episode_config)
67
+ Config::Episode.load_file(episode_config)
68
+ else
69
+ {}
70
+ end
67
71
 
68
72
  options.require.each { require(it) }
69
73
 
@@ -4,7 +4,7 @@ module ScreenKit
4
4
  module ContentType
5
5
  def self.video = %w[mp4 webm mov]
6
6
  def self.audio = %w[mp3 wav m4a aac aiff]
7
- def self.image = %w[gif jpg jpeg png]
7
+ def self.image = %w[gif jpg jpeg png tiff]
8
8
  def self.demotape = %w[tape]
9
9
 
10
10
  def self.all = video + image + demotape
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module ScreenKit
4
6
  module CoreExt
5
7
  refine JSON.singleton_class do
@@ -7,6 +7,8 @@ module ScreenKit
7
7
  unicode_normalize(:nfkd)
8
8
  .delete("'")
9
9
  .gsub(/[^\x00-\x7F]/, "")
10
+ .gsub(/([a-z\d])([A-Z])/, '\1-\2')
11
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1-\2')
10
12
  .gsub(/[^-\w]+/xim, "-")
11
13
  .tr("_", "-")
12
14
  .gsub(/-+/xm, "-")
@@ -14,6 +16,10 @@ module ScreenKit
14
16
  .downcase
15
17
  end
16
18
 
19
+ def underscore
20
+ dasherize.tr("-", "_")
21
+ end
22
+
17
23
  def camelize(first_letter = :upper)
18
24
  split(/_|-/).map.with_index do |part, index|
19
25
  if index.zero? && first_letter == :lower
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "core_ext/string"
4
+ require_relative "core_ext/json"
@@ -27,7 +27,8 @@ module ScreenKit
27
27
  "--fps", 24,
28
28
  "--overwrite",
29
29
  "--output-path", output_path,
30
- log_path:
30
+ log_path:,
31
+ chdir: demotape_path.parent.parent
31
32
  end
32
33
 
33
34
  def options_to_args(options)
@@ -46,18 +46,11 @@ module ScreenKit
46
46
  end
47
47
 
48
48
  def tts_engine
49
- @tts_engine ||=
50
- if options.tts_preset
51
- tts_engines.find do |engine|
52
- engine.id == options.tts_preset && engine.available?
53
- end
54
- else
55
- tts_engines.find(&:available?)
56
- end
49
+ tts_engines.first
57
50
  end
58
51
 
59
- def tts_engines
60
- @tts_engines ||= begin
52
+ def tts_config
53
+ @tts_config ||= begin
61
54
  project_tts = if project_config.tts.is_a?(Hash)
62
55
  [project_config.tts]
63
56
  else
@@ -70,10 +63,23 @@ module ScreenKit
70
63
  Array(config.tts)
71
64
  end
72
65
 
73
- (episode_tts + project_tts).map do |opts|
74
- TTS.const_get(opts[:engine].camelize)
75
- .new(**opts.except(:engine), api_key: options.tts_api_key)
76
- end
66
+ episode_tts + project_tts
67
+ end
68
+ end
69
+
70
+ def tts_engines
71
+ @tts_engines ||= tts_config.filter_map do |opts|
72
+ next unless opts[:enabled]
73
+
74
+ api_key = options.tts_api_key
75
+ tts_class = TTS.const_get(opts[:engine].camelize)
76
+ tts_preset = options.tts_preset.to_s
77
+
78
+ next if !tts_preset.empty? && tts_preset != opts[:id]
79
+ next unless tts_class.available?(api_key:, **opts)
80
+
81
+ opts = opts.except(:engine, :enabled)
82
+ tts_class.new(**opts, api_key:, segments:)
77
83
  end
78
84
  end
79
85
 
@@ -177,6 +177,10 @@ module ScreenKit
177
177
  log_path:
178
178
  end
179
179
 
180
+ def script_content
181
+ @script_content ||= script_path.read if script_path.file?
182
+ end
183
+
180
184
  def create_voiceover(log_path:)
181
185
  return if voiceover_path&.file? && !episode.options.overwrite
182
186
  return unless script_path.file?
@@ -185,7 +189,7 @@ module ScreenKit
185
189
  FileUtils.mkdir_p(voiceover_path.dirname)
186
190
 
187
191
  episode.tts_engine.generate(
188
- text: script_path.read,
192
+ text: script_content,
189
193
  output_path: voiceover_path,
190
194
  log_path:
191
195
  )
@@ -9,8 +9,12 @@ module ScreenKit
9
9
  # The path to the input video file.
10
10
  attr_reader :input_path
11
11
 
12
- def initialize(input_path:)
12
+ # The path to the log file.
13
+ attr_reader :log_path
14
+
15
+ def initialize(input_path:, log_path: nil)
13
16
  @input_path = input_path
17
+ @log_path = log_path
14
18
  end
15
19
 
16
20
  def export(output_path)
@@ -26,7 +30,8 @@ module ScreenKit
26
30
  "-r", "24",
27
31
  "-c:v", "libx264",
28
32
  "-y",
29
- output_path
33
+ output_path,
34
+ log_path:
30
35
  end
31
36
  end
32
37
  end
@@ -1,106 +1,11 @@
1
1
  ---
2
2
  # yaml-language-server: $schema=https://screenkit.dev/schemas/episode.json
3
3
 
4
- # The episode title. When there are no linebreaks, they will be inferred based
5
- # on the approximate text width.
6
- title: "CREATING SCREENCASTS WITH SCREENKIT"
7
-
8
- # Callouts are informational boxes that on the screen during the
9
- # video (also known as lower thirds).
10
- #
11
- # The project's configuration can define the callout structure, while the
12
- # episode configuration can define the content and timing.
13
- callouts_styles:
14
- - type: info
15
- title: "ScreenKit"
16
- body: "Visit https://github.com/fnando/screenkit to learn more."
17
- starts_at: 3
18
- duration: 5
19
- width: 600
20
-
21
- # The background music to be played during the episode.
22
- # When not set, the backtrack will be defined by using the project's
23
- # `backtrack_dir` configuration, and a track will be randomly selected.
24
- # To disable the backtrack entirely, use `backtrack: false`.
25
- # backtrack: resources/backtrack.mp3
26
-
27
- # Define episode-specific scene configurations.
28
- # When present, this will have higher precedence over the project's
29
- # configuration, so you can have different intro/outro for each episode.
30
- # scenes:
31
- # intro:
32
- # # The duration of the intro scene in seconds.
33
- # duration: 5.5
34
- #
35
- # # The fade-in duration in seconds.
36
- # fade_in: 0
37
- #
38
- # # The fade-out duration in seconds.
39
- # fade_out: 0.5
40
- #
41
- # # The background color/image of the intro scene.
42
- # background: "#100f50"
43
- #
44
- # # The title text to be displayed in the intro scene.
45
- # title:
46
- # x: 100
47
- # y: 300
48
- # font_path: open-sans/OpenSans-ExtraBold.ttf
49
- # size: 144
50
- # color: "#ffffff"
51
- #
52
- # # The logo to be displayed in the video intro.
53
- # # Works best with a transparent PNG file in high resolution (at least 2x the
54
- # # size it'll be displayed).
55
- # logo:
56
- # path: images/logo.png
57
- # x: 100
58
- # y: 200
59
- # width: 200
60
- # height: 34
61
- #
62
- # # The sound to be played along with the logo in the intro.
63
- # # This must correspond to a sound file in the sounds directory, with any
64
- # # audio extension (e.g. mp3, m4a, wav).
65
- # sound: sounds/chime.mp3
66
- #
67
- # outro:
68
- # # The duration of the outro scene in seconds.
69
- # duration: 3.5
70
- #
71
- # # The fade-in duration in seconds.
72
- # fade_in: 0.5
73
- #
74
- # # The fade-out duration in seconds.
75
- # fade_out: 0.5
76
- #
77
- # # The background color/image of the outro scene.
78
- # background: "#100f50"
79
- #
80
- # # The sound to be played along with the logo in the outro.
81
- # sound: sounds/chime.mp3
82
- #
83
- # # The logo to be displayed in the video outro.
84
- # logo:
85
- # path: images/logo.png
86
- # x: center
87
- # y: center
88
- # width: 500
89
- #
90
- # segment:
91
- # # The duration of the crossover transition between segments in seconds.
92
- # crossfade_duration: 0.5
93
-
94
- # Define episode-specific voice configuration.
95
- # When present, this will have higher precedence over the project's
96
- # configuration, so you can have different intro/outro segments for each
97
- # episode.
98
- # tts:
99
- # engine: elevenlabs
100
- # voice_id: 56AoDkrOh6qfVPDXZ7Pt
101
- # language_code: en
102
- # voice_settings:
103
- # speed: 0.9
104
- # stability: 0.5
105
- # similarity: 0.75
106
- # style: 0.0
4
+ # The episode title (displayed in intro scene).
5
+ # Line breaks are inferred based on approximate text width.
6
+ title: <%= options.title.to_yaml[4..] %>
7
+
8
+ # Episode-specific overrides for global configuration.
9
+ # Use an editor with JSON Schema support to see available options and
10
+ # autocomplete.
11
+ # Schema: https://screenkit.dev/schemas/episode.json
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: Screenkit
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ episode:
8
+ type: string
9
+ description: "Episode number (e.g. 001)"
10
+ required: true
11
+ tts_preset:
12
+ description: "TTS preset name"
13
+ default: ""
14
+ type: choice
15
+ options:
16
+ - ""
17
+ - say
18
+ - espeak
19
+ - eleven_labs
20
+ - eleven_labs_mp3_192k
21
+ match:
22
+ type: string
23
+ description: "Match pattern for segments (e.g. 001)"
24
+ default: ""
25
+ overwrite:
26
+ type: boolean
27
+ description: "Regenerate files"
28
+
29
+ jobs:
30
+ screenkit:
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ contents: read
34
+ actions: write
35
+
36
+ steps:
37
+ - name: Checkout repository
38
+ uses: actions/checkout@v3
39
+
40
+ - name: Run Screenkit
41
+ uses: fnando/screenkit@v1
42
+ with:
43
+ episode: ${{ github.event.inputs.episode }}
44
+ tts_preset: ${{ github.event.inputs.tts_preset }}
45
+ tts_api_key: ${{ secrets.TTS_API_KEY }}
46
+ overwrite: ${{ github.event.inputs.overwrite }}
47
+ match: ${{ github.event.inputs.match }}
48
+ github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -22,6 +22,7 @@ module ScreenKit
22
22
  def copy_files
23
23
  copy_file "screenkit.yml"
24
24
  directory "resources", exclude_pattern: /DS_Store/
25
+ directory ".github", exclude_pattern: /DS_Store/
25
26
  end
26
27
 
27
28
  def bundle_install
@@ -32,6 +33,12 @@ module ScreenKit
32
33
  end
33
34
  end
34
35
 
36
+ def instructions
37
+ cmd = set_color("screenkit episode new --title TITLE", :blue)
38
+ path = set_color(destination_root, :blue)
39
+ say "\nTo create a new episode, run #{cmd} from #{path}"
40
+ end
41
+
35
42
  no_commands do
36
43
  # Add helper methods here
37
44
  end
@@ -20,9 +20,9 @@ module ScreenKit
20
20
  !`which #{command}`.strip.empty?
21
21
  end
22
22
 
23
- def run_command(command, *args, log_path: nil)
23
+ def run_command(command, *args, log_path: nil, **)
24
24
  args = args.flatten.compact.map(&:to_s)
25
- stdout, stderr, status = Open3.capture3(command, *args)
25
+ stdout, stderr, status = Open3.capture3(command, *args, **)
26
26
  exit_code = status.exitstatus
27
27
 
28
28
  if exit_code&.nonzero?
@@ -3,22 +3,48 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Base
6
+ require_relative "../core_ext"
7
+ require_relative "../schema_validator"
8
+
6
9
  extend SchemaValidator
7
10
 
11
+ using CoreExt
12
+
8
13
  # Additional options for the tts engine.
9
14
  attr_reader :options
10
15
 
11
16
  # The preset name for the tts engine.
12
17
  attr_reader :id
13
18
 
14
- def initialize(id: nil, enabled: true, **options)
15
- @enabled = enabled
19
+ # The list of segments.
20
+ # This is available so that engines can contextually generate audio, for
21
+ # instance, by providing previous/next text (e.g. Eleven Labs).
22
+ attr_reader :segments
23
+
24
+ # The API key for the tts engine, if applicable.
25
+ attr_reader :api_key
26
+
27
+ # Detects if the tts engine is available.
28
+ def self.available?(**)
29
+ false
30
+ end
31
+
32
+ def self.engine_name
33
+ name.split("::").last.underscore
34
+ end
35
+
36
+ def initialize(id: nil, segments: nil, api_key: nil, **options)
37
+ @segments = Array(segments)
16
38
  @options = options
17
39
  @id = id
40
+ @api_key = api_key
18
41
  end
19
42
 
20
- def enabled?
21
- @enabled
43
+ def redact_file(path, text)
44
+ return unless File.file?(path)
45
+
46
+ content = File.read(path).gsub(text, "[REDACTED]")
47
+ File.write(path, content)
22
48
  end
23
49
  end
24
50
  end
@@ -8,21 +8,16 @@ module ScreenKit
8
8
  .join("screenkit/schemas/tts/elevenlabs.json")
9
9
  end
10
10
 
11
- # The Eleven Labs API key.
12
- attr_reader :api_key
13
-
14
- def initialize(api_key:, **)
15
- super(**)
16
- @api_key = api_key
11
+ def self.available?(api_key: nil, **)
12
+ api_key.to_s.empty?
17
13
  end
18
14
 
19
- def available?
20
- enabled? && !api_key.to_s.empty?
15
+ def all_texts
16
+ @all_texts ||= segments.map(&:script_content)
21
17
  end
22
18
 
23
19
  def generate(output_path:, text:, log_path: nil)
24
- self.class.validate!(options)
25
- voice_id = options.delete(:voice_id)
20
+ voice_id = options[:voice_id]
26
21
 
27
22
  if log_path
28
23
  File.open(log_path, "w") do |f|
@@ -32,9 +27,23 @@ module ScreenKit
32
27
 
33
28
  require "aitch"
34
29
 
30
+ Aitch.configure do |config|
31
+ config.logger = Logger.new(log_path) if log_path
32
+ end
33
+
34
+ current_index = all_texts.index { it == text }
35
+
36
+ if current_index
37
+ previous_text = all_texts[current_index - 1]
38
+ next_text = all_texts[current_index + 1]
39
+ end
40
+
41
+ params = options.merge(text:, previous_text:, next_text:)
42
+ .except(:voice_id)
43
+
35
44
  response = Aitch.post(
36
45
  url: "https://api.elevenlabs.io/v1/text-to-speech/#{voice_id}",
37
- body: JSON.dump(options.merge(text:)),
46
+ body: JSON.dump(params),
38
47
  options: {expect: 200},
39
48
  headers: {
40
49
  "content-type": "application/json",
@@ -44,6 +53,8 @@ module ScreenKit
44
53
  )
45
54
 
46
55
  File.binwrite(output_path, response.body)
56
+ ensure
57
+ redact_file(log_path, api_key)
47
58
  end
48
59
  end
49
60
  end
@@ -3,14 +3,14 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Espeak < Base
6
- include Shell
6
+ extend Shell
7
7
 
8
- def self.schema_path
9
- ScreenKit.root_dir.join("screenkit/schemas/tts/espeak.json")
8
+ def self.available?(**)
9
+ command_exist?("espeak")
10
10
  end
11
11
 
12
- def available?
13
- enabled? && command_exist?("espeak")
12
+ def self.schema_path
13
+ ScreenKit.root_dir.join("screenkit/schemas/tts/espeak.json")
14
14
  end
15
15
 
16
16
  def generate(text:, output_path:, log_path: nil)
@@ -18,12 +18,12 @@ module ScreenKit
18
18
 
19
19
  {voice: nil, rate: nil}.merge(options) => {voice:, rate:}
20
20
 
21
- run_command "espeak",
22
- (["-v", voice] if voice),
23
- (["-s", rate] if rate),
24
- "-w", output_path.sub_ext(".wav"),
25
- text,
26
- log_path:
21
+ self.class.run_command "espeak",
22
+ (["-v", voice] if voice),
23
+ (["-s", rate] if rate),
24
+ "-w", output_path.sub_ext(".wav"),
25
+ text,
26
+ log_path:
27
27
  end
28
28
  end
29
29
  end
@@ -3,14 +3,14 @@
3
3
  module ScreenKit
4
4
  module TTS
5
5
  class Say < Base
6
- include Shell
6
+ extend Shell
7
7
 
8
8
  def self.schema_path
9
9
  ScreenKit.root_dir.join("screenkit/schemas/tts/say.json")
10
10
  end
11
11
 
12
- def available?
13
- enabled? && command_exist?("say")
12
+ def self.available?(**)
13
+ command_exist?("say")
14
14
  end
15
15
 
16
16
  def generate(text:, output_path:, log_path: nil)
@@ -18,12 +18,12 @@ module ScreenKit
18
18
 
19
19
  {voice: nil, rate: nil}.merge(options) => {voice:, rate:}
20
20
 
21
- run_command "say",
22
- (["-v", voice] if voice),
23
- (["-r", rate] if rate),
24
- "-o", output_path.sub_ext(".aiff"),
25
- text,
26
- log_path:
21
+ self.class.run_command "say",
22
+ (["-v", voice] if voice),
23
+ (["-r", rate] if rate),
24
+ "-o", output_path.sub_ext(".aiff"),
25
+ text,
26
+ log_path:
27
27
  end
28
28
  end
29
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScreenKit
4
- VERSION = "0.0.6"
4
+ VERSION = "0.0.7"
5
5
  end
data/lib/screenkit.rb CHANGED
@@ -1,3 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "screen_kit"
3
+ require "thor"
4
+ require "thor/completion"
5
+ require "yaml"
6
+ require "json-schema"
7
+ require "mini_magick"
8
+ require "pathname"
9
+ require "tty-spinner"
10
+ require "etc"
11
+ require "securerandom"
12
+ require "demo_tape/duration"
13
+
14
+ module ScreenKit
15
+ require_relative "screenkit/version"
16
+ require_relative "screenkit/duration"
17
+ require_relative "screenkit/core_ext"
18
+ require_relative "screenkit/content_type"
19
+ require_relative "screenkit/anchor"
20
+ require_relative "screenkit/banner"
21
+ require_relative "screenkit/time_formatter"
22
+ require_relative "screenkit/spacing"
23
+ require_relative "screenkit/watermark"
24
+ require_relative "screenkit/spinner"
25
+ require_relative "screenkit/shell"
26
+ require_relative "screenkit/schema_validator"
27
+ require_relative "screenkit/generators/project"
28
+ require_relative "screenkit/generators/episode"
29
+ require_relative "screenkit/config/base"
30
+ require_relative "screenkit/config/project"
31
+ require_relative "screenkit/config/episode"
32
+ require_relative "screenkit/callout"
33
+ require_relative "screenkit/callout/text_style"
34
+ require_relative "screenkit/callout/styles/base"
35
+ require_relative "screenkit/transition"
36
+ require_relative "screenkit/parallel_processor"
37
+ require_relative "screenkit/cli"
38
+ require_relative "screenkit/cli/base"
39
+ require_relative "screenkit/cli/episode"
40
+ require_relative "screenkit/cli/root"
41
+ require_relative "screenkit/animation_filters"
42
+ require_relative "screenkit/tts/base"
43
+ require_relative "screenkit/path_lookup"
44
+ require_relative "screenkit/sound"
45
+ require_relative "screenkit/utils"
46
+ require_relative "screenkit/logfile"
47
+ require_relative "screenkit/exporter/intro"
48
+ require_relative "screenkit/exporter/outro"
49
+ require_relative "screenkit/exporter/demotape"
50
+ require_relative "screenkit/exporter/episode"
51
+ require_relative "screenkit/exporter/segment"
52
+ require_relative "screenkit/exporter/image"
53
+ require_relative "screenkit/exporter/video"
54
+
55
+ require_files = lambda do |pattern|
56
+ Gem.find_files_from_load_path(pattern).each do |path|
57
+ next if path.include?("test")
58
+
59
+ require(path)
60
+ end
61
+ end
62
+
63
+ # Load all files that may be available as plugins.
64
+ require_files.call("screenkit/callout/styles/*.rb")
65
+ require_files.call("screenkit/tts/*.rb")
66
+
67
+ def self.root_dir
68
+ @root_dir ||= Pathname(__dir__)
69
+ end
70
+
71
+ # Raised when the configuration schema is invalid.
72
+ InvalidConfigSchemaError = Class.new(StandardError)
73
+
74
+ # Raised when a file is not found.
75
+ FileNotFoundError = Class.new(StandardError)
76
+
77
+ # Raised when a file entry is not found in the lookup.
78
+ FileEntryNotFoundError = Class.new(StandardError)
79
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: screenkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Vieira
@@ -276,10 +276,10 @@ files:
276
276
  - LICENSE.md
277
277
  - README.md
278
278
  - Rakefile
279
+ - action.yml
279
280
  - bin/console
280
281
  - bin/setup
281
282
  - exe/screenkit
282
- - lib/screen_kit.rb
283
283
  - lib/screenkit.rb
284
284
  - lib/screenkit/anchor.rb
285
285
  - lib/screenkit/animation_filters.rb
@@ -298,6 +298,7 @@ files:
298
298
  - lib/screenkit/config/episode.rb
299
299
  - lib/screenkit/config/project.rb
300
300
  - lib/screenkit/content_type.rb
301
+ - lib/screenkit/core_ext.rb
301
302
  - lib/screenkit/core_ext/json.rb
302
303
  - lib/screenkit/core_ext/string.rb
303
304
  - lib/screenkit/duration.rb
@@ -314,6 +315,7 @@ files:
314
315
  - lib/screenkit/generators/episode/content/001.tape
315
316
  - lib/screenkit/generators/episode/scripts/001.txt
316
317
  - lib/screenkit/generators/project.rb
318
+ - lib/screenkit/generators/project/.github/screenkit.yml
317
319
  - lib/screenkit/generators/project/Gemfile.erb
318
320
  - lib/screenkit/generators/project/resources/backtracks/default.aac
319
321
  - lib/screenkit/generators/project/resources/fonts/open-sans/OFL.txt
@@ -385,10 +387,10 @@ metadata:
385
387
  rubygems_mfa_required: 'true'
386
388
  homepage_uri: https://github.com/fnando/screenkit
387
389
  bug_tracker_uri: https://github.com/fnando/screenkit/issues
388
- source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.6
389
- changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.6/CHANGELOG.md
390
- documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.6/README.md
391
- license_uri: https://github.com/fnando/screenkit/tree/v0.0.6/LICENSE.md
390
+ source_code_uri: https://github.com/fnando/screenkit/tree/v0.0.7
391
+ changelog_uri: https://github.com/fnando/screenkit/tree/v0.0.7/CHANGELOG.md
392
+ documentation_uri: https://github.com/fnando/screenkit/tree/v0.0.7/README.md
393
+ license_uri: https://github.com/fnando/screenkit/tree/v0.0.7/LICENSE.md
392
394
  rdoc_options: []
393
395
  require_paths:
394
396
  - lib
data/lib/screen_kit.rb DELETED
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "thor"
4
- require "thor/completion"
5
- require "yaml"
6
- require "json-schema"
7
- require "mini_magick"
8
- require "pathname"
9
- require "tty-spinner"
10
- require "etc"
11
- require "securerandom"
12
- require "demo_tape/duration"
13
-
14
- module ScreenKit
15
- require_relative "screenkit/version"
16
- require_relative "screenkit/duration"
17
- require_relative "screenkit/core_ext/string"
18
- require_relative "screenkit/core_ext/json"
19
- require_relative "screenkit/content_type"
20
- require_relative "screenkit/anchor"
21
- require_relative "screenkit/banner"
22
- require_relative "screenkit/time_formatter"
23
- require_relative "screenkit/spacing"
24
- require_relative "screenkit/watermark"
25
- require_relative "screenkit/spinner"
26
- require_relative "screenkit/shell"
27
- require_relative "screenkit/schema_validator"
28
- require_relative "screenkit/generators/project"
29
- require_relative "screenkit/generators/episode"
30
- require_relative "screenkit/config/base"
31
- require_relative "screenkit/config/project"
32
- require_relative "screenkit/config/episode"
33
- require_relative "screenkit/callout"
34
- require_relative "screenkit/callout/text_style"
35
- require_relative "screenkit/callout/styles/base"
36
- require_relative "screenkit/transition"
37
- require_relative "screenkit/parallel_processor"
38
- require_relative "screenkit/cli"
39
- require_relative "screenkit/cli/base"
40
- require_relative "screenkit/cli/episode"
41
- require_relative "screenkit/cli/root"
42
- require_relative "screenkit/animation_filters"
43
- require_relative "screenkit/tts/base"
44
- require_relative "screenkit/path_lookup"
45
- require_relative "screenkit/sound"
46
- require_relative "screenkit/utils"
47
- require_relative "screenkit/logfile"
48
- require_relative "screenkit/exporter/intro"
49
- require_relative "screenkit/exporter/outro"
50
- require_relative "screenkit/exporter/demotape"
51
- require_relative "screenkit/exporter/episode"
52
- require_relative "screenkit/exporter/segment"
53
- require_relative "screenkit/exporter/image"
54
- require_relative "screenkit/exporter/video"
55
-
56
- require_files = lambda do |pattern|
57
- Gem.find_files_from_load_path(pattern).each do |path|
58
- next if path.include?("test")
59
-
60
- require(path)
61
- end
62
- end
63
-
64
- # Load all files that may be available as plugins.
65
- require_files.call("screenkit/callout/styles/*.rb")
66
- require_files.call("screenkit/tts/*.rb")
67
-
68
- def self.root_dir
69
- @root_dir ||= Pathname(__dir__)
70
- end
71
-
72
- # Raised when the configuration schema is invalid.
73
- InvalidConfigSchemaError = Class.new(StandardError)
74
-
75
- # Raised when a file is not found.
76
- FileNotFoundError = Class.new(StandardError)
77
-
78
- # Raised when a file entry is not found in the lookup.
79
- FileEntryNotFoundError = Class.new(StandardError)
80
- end