token_reel 0.2.0 → 0.3.0
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/release.yml +57 -0
- data/.github/workflows/version-bump.yml +62 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/lib/token_reel/cli.rb +2 -0
- data/lib/token_reel/config.rb +4 -1
- data/lib/token_reel/timeline.rb +54 -6
- data/lib/token_reel/version.rb +1 -1
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8a6442883c40d8cc57ff6c4763fde5fbfa7b1c5b018a77023bc8a38f41de047
|
|
4
|
+
data.tar.gz: e98f5f9e2cfce2c6cfe9cb32888ef106a492ed4b6b671e9d80d0412dab1d8734
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d0f2bf7add9fd157f157e2c34f462b37bae1d0a8689adb30390b8bd4bfa3e944cdcda8852fb0437298bd3ee14926b52775d4abf8d98a3382230296ca4bf05bc
|
|
7
|
+
data.tar.gz: 5156c0c8b53610b545be54f43f2c66ee86b2eaa127347a8a9d84c2c8a93ab99ffa0132a5f194e5e6606f634d7a891de032ca09cb4959f77c4c2c318fbeca8085
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to RubyGems whenever a "v*" tag is pushed (see
|
|
4
|
+
# .github/workflows/version-bump.yml, which creates that tag). Uses
|
|
5
|
+
# RubyGems' Trusted Publishing (OIDC) -- no API key stored in this repo.
|
|
6
|
+
# One-time setup required on rubygems.org: Settings > Trusted Publishers
|
|
7
|
+
# on the token_reel gem, with owner "stringsn88keys", repository
|
|
8
|
+
# "token_reel", workflow filename "release.yml", environment "release".
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags:
|
|
13
|
+
- "v*"
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
name: Verify build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Install ImageMagick
|
|
26
|
+
run: sudo apt-get update && sudo apt-get install -y imagemagick
|
|
27
|
+
|
|
28
|
+
- uses: ruby/setup-ruby@v1
|
|
29
|
+
with:
|
|
30
|
+
ruby-version: "3.3"
|
|
31
|
+
bundler-cache: true
|
|
32
|
+
|
|
33
|
+
- name: Run specs
|
|
34
|
+
run: bundle exec rake spec
|
|
35
|
+
|
|
36
|
+
- name: Build gem
|
|
37
|
+
run: gem build token_reel.gemspec
|
|
38
|
+
|
|
39
|
+
release:
|
|
40
|
+
name: Publish to RubyGems
|
|
41
|
+
needs: test
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment: release
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v5
|
|
49
|
+
with:
|
|
50
|
+
persist-credentials: false
|
|
51
|
+
|
|
52
|
+
- uses: ruby/setup-ruby@v1
|
|
53
|
+
with:
|
|
54
|
+
bundler-cache: true
|
|
55
|
+
ruby-version: ruby
|
|
56
|
+
|
|
57
|
+
- uses: rubygems/release-gem@v1
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Version Bump
|
|
2
|
+
|
|
3
|
+
# Bumps the gem version when a PR is merged to main with a "major",
|
|
4
|
+
# "minor", or "patch" label, then tags the resulting commit. Pushing
|
|
5
|
+
# that tag triggers .github/workflows/release.yml, which publishes the
|
|
6
|
+
# gem to RubyGems.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
pull_request:
|
|
10
|
+
types: [closed]
|
|
11
|
+
branches: [main]
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
bump:
|
|
18
|
+
if: github.event.pull_request.merged == true
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: Determine version bump from labels
|
|
22
|
+
id: bump
|
|
23
|
+
env:
|
|
24
|
+
LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
25
|
+
run: |
|
|
26
|
+
has_label() { echo "$LABELS_JSON" | jq -e --arg l "$1" 'index($l) != null' > /dev/null; }
|
|
27
|
+
if has_label major; then
|
|
28
|
+
part=major
|
|
29
|
+
elif has_label minor; then
|
|
30
|
+
part=minor
|
|
31
|
+
elif has_label patch; then
|
|
32
|
+
part=patch
|
|
33
|
+
else
|
|
34
|
+
part=""
|
|
35
|
+
fi
|
|
36
|
+
echo "part=$part" >> "$GITHUB_OUTPUT"
|
|
37
|
+
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
if: steps.bump.outputs.part != ''
|
|
40
|
+
with:
|
|
41
|
+
ref: main
|
|
42
|
+
fetch-depth: 0
|
|
43
|
+
|
|
44
|
+
- uses: ruby/setup-ruby@v1
|
|
45
|
+
if: steps.bump.outputs.part != ''
|
|
46
|
+
with:
|
|
47
|
+
ruby-version: "3.3"
|
|
48
|
+
bundler-cache: true
|
|
49
|
+
|
|
50
|
+
- name: Bump version
|
|
51
|
+
if: steps.bump.outputs.part != ''
|
|
52
|
+
run: bundle exec rake "version:${{ steps.bump.outputs.part }}"
|
|
53
|
+
|
|
54
|
+
- name: Commit and tag
|
|
55
|
+
if: steps.bump.outputs.part != ''
|
|
56
|
+
run: |
|
|
57
|
+
new_version=$(ruby -e 'puts File.read("lib/token_reel/version.rb")[/VERSION = "(.*)"/, 1]')
|
|
58
|
+
git config user.name "github-actions[bot]"
|
|
59
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
60
|
+
git commit -am "Bump version to ${new_version}"
|
|
61
|
+
git tag "v${new_version}"
|
|
62
|
+
git push origin main "v${new_version}"
|
data/README.md
CHANGED
|
@@ -114,6 +114,8 @@ Run `token_reel --help` for the full flag list. The important ones:
|
|
|
114
114
|
| `--font NAME` | exact ImageMagick font name | auto-detected monospace |
|
|
115
115
|
| `--fps N` | GIF frame rate | `12` |
|
|
116
116
|
| `--hold N` | seconds to hold on the finished frame | `1.5` |
|
|
117
|
+
| `--variability` | jitter each token's delay +/-10/20/30% (normal distribution) for a more natural, less metronomic stream | off |
|
|
118
|
+
| `--seed N` | RNG seed for `--variability`, for reproducible output | random |
|
|
117
119
|
| `-o, --out PATH` | output GIF path | `token_reel.gif` |
|
|
118
120
|
|
|
119
121
|
## Ruby API
|
|
@@ -165,6 +167,45 @@ Frames are sampled at `--fps` and identical consecutive frames are
|
|
|
165
167
|
collapsed into a single frame with a longer delay, so a long `--ttft`
|
|
166
168
|
or a slow `--tps` doesn't blow up the frame count or file size.
|
|
167
169
|
|
|
170
|
+
By default every token in a stream lands at an exact, metronomic
|
|
171
|
+
interval. Pass `--variability` to jitter each token's delay instead:
|
|
172
|
+
per token, one of +/-10%, +/-20%, or +/-30% is picked at random and
|
|
173
|
+
applied as a normally-distributed offset around the base interval, so
|
|
174
|
+
some tokens land a little early and others a little late -- closer to
|
|
175
|
+
how a real model streams. Pair it with `--seed N` to reproduce the
|
|
176
|
+
exact same jittered timeline across runs.
|
|
177
|
+
|
|
178
|
+
## Releasing
|
|
179
|
+
|
|
180
|
+
Version bumps and RubyGems publishing are both automated, driven off
|
|
181
|
+
PR labels:
|
|
182
|
+
|
|
183
|
+
1. Label a PR `major`, `minor`, or `patch` (semver meaning, same as
|
|
184
|
+
the `rake version:*` tasks below) before merging it to `main`.
|
|
185
|
+
2. On merge, [`version-bump.yml`](.github/workflows/version-bump.yml)
|
|
186
|
+
bumps `lib/token_reel/version.rb` accordingly, commits it to
|
|
187
|
+
`main`, and pushes a matching `vX.Y.Z` tag.
|
|
188
|
+
3. That tag push triggers [`release.yml`](.github/workflows/release.yml),
|
|
189
|
+
which runs the test suite and then publishes the gem to RubyGems
|
|
190
|
+
using [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/)
|
|
191
|
+
(OIDC -- no API key stored in this repo).
|
|
192
|
+
|
|
193
|
+
A PR merged without one of those labels doesn't bump the version or
|
|
194
|
+
release anything.
|
|
195
|
+
|
|
196
|
+
Trusted Publishing needs a one-time setup on rubygems.org, under the
|
|
197
|
+
gem's *Trusted Publishers* settings: owner `stringsn88keys`, repository
|
|
198
|
+
`token_reel`, workflow filename `release.yml`, environment `release`.
|
|
199
|
+
|
|
200
|
+
The version can also be bumped locally without releasing:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
rake version # print the current version
|
|
204
|
+
rake version:patch # x.x.X -- backwards-compatible fixes
|
|
205
|
+
rake version:minor # x.X.0 -- backwards-compatible features
|
|
206
|
+
rake version:major # X.0.0 -- breaking changes
|
|
207
|
+
```
|
|
208
|
+
|
|
168
209
|
## License
|
|
169
210
|
|
|
170
211
|
MIT
|
data/Rakefile
CHANGED
data/lib/token_reel/cli.rb
CHANGED
|
@@ -50,6 +50,8 @@ module TokenReel
|
|
|
50
50
|
o.on("--hold N", Float, "Seconds to hold the final frame (default: #{config.hold})") { |v| config.hold = v }
|
|
51
51
|
o.on("--fps N", Integer, "GIF frame rate (default: #{config.fps})") { |v| config.fps = v }
|
|
52
52
|
o.on("--loop N", Integer, "GIF loop count, 0 = forever (default: #{config.loop_count})") { |v| config.loop_count = v }
|
|
53
|
+
o.on("--variability", "Jitter each token's delay by a random +/-10/20/30% (normal distribution)") { config.variability = true }
|
|
54
|
+
o.on("--seed N", Integer, "RNG seed for --variability, for reproducible output (default: random)") { |v| config.seed = v }
|
|
53
55
|
|
|
54
56
|
o.separator ""
|
|
55
57
|
o.separator "Look:"
|
data/lib/token_reel/config.rb
CHANGED
|
@@ -9,7 +9,8 @@ module TokenReel
|
|
|
9
9
|
:cols, :rows, :font, :font_size, :fps,
|
|
10
10
|
:theme, :hold, :loop_count,
|
|
11
11
|
:label, :thinking_label, :title, :cursor_char,
|
|
12
|
-
:out, :keep_frames
|
|
12
|
+
:out, :keep_frames,
|
|
13
|
+
:variability, :seed
|
|
13
14
|
|
|
14
15
|
def initialize
|
|
15
16
|
@prompt = ""
|
|
@@ -35,6 +36,8 @@ module TokenReel
|
|
|
35
36
|
@cursor_char = "\u258A" # "▊"
|
|
36
37
|
@out = "token_reel.gif"
|
|
37
38
|
@keep_frames = false
|
|
39
|
+
@variability = false # jitter each token's delay by a random +/-10/20/30% (normal distribution)
|
|
40
|
+
@seed = nil # RNG seed for --variability; nil = a fresh random seed each run
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def validate!
|
data/lib/token_reel/timeline.rb
CHANGED
|
@@ -20,6 +20,11 @@ module TokenReel
|
|
|
20
20
|
BLINK_HZ = 2.0 # cursor toggles this many times per second
|
|
21
21
|
DOT_INTERVAL = 0.35 # seconds between "thinking..." dot ticks
|
|
22
22
|
|
|
23
|
+
# When config.variability is on, each token's delay is independently
|
|
24
|
+
# jittered by a normally-distributed offset, bounded to +/- one of
|
|
25
|
+
# these percentages (picked at random per token) of its base interval.
|
|
26
|
+
JITTER_PCTS = [0.10, 0.20, 0.30].freeze
|
|
27
|
+
|
|
23
28
|
attr_reader :prompt_tokens, :reasoning_tokens, :response_tokens,
|
|
24
29
|
:prompt_full, :reasoning_full, :response_full,
|
|
25
30
|
:prompt_done_t, :thinking_end_t, :reasoning_end_t, :stream_end_t, :duration
|
|
@@ -37,10 +42,20 @@ module TokenReel
|
|
|
37
42
|
@reasoning_interval = config.reasoning_tps.to_f.positive? ? 1.0 / config.reasoning_tps : 1.0 / config.tps
|
|
38
43
|
@response_interval = 1.0 / config.tps
|
|
39
44
|
|
|
40
|
-
@
|
|
45
|
+
@rng = config.variability ? Random.new(config.seed || Random.new_seed) : nil
|
|
46
|
+
|
|
47
|
+
# Cumulative reveal times per token: offsets[n] is the moment the
|
|
48
|
+
# n-th token has fully appeared. With variability off this is just
|
|
49
|
+
# n * interval; with it on, each step is independently jittered,
|
|
50
|
+
# so the array can't be derived by a plain multiply anymore.
|
|
51
|
+
@prompt_offsets = cumulative_offsets(prompt_tokens.size, @prompt_interval)
|
|
52
|
+
@reasoning_offsets = cumulative_offsets(reasoning_tokens.size, @reasoning_interval)
|
|
53
|
+
@response_offsets = cumulative_offsets(response_tokens.size, @response_interval)
|
|
54
|
+
|
|
55
|
+
@prompt_done_t = @prompt_offsets.last
|
|
41
56
|
@thinking_end_t = prompt_done_t + config.ttft.to_f
|
|
42
|
-
@reasoning_end_t = thinking_end_t + @
|
|
43
|
-
@stream_end_t = reasoning_end_t + @
|
|
57
|
+
@reasoning_end_t = thinking_end_t + @reasoning_offsets.last
|
|
58
|
+
@stream_end_t = reasoning_end_t + @response_offsets.last
|
|
44
59
|
@duration = stream_end_t + config.hold.to_f
|
|
45
60
|
end
|
|
46
61
|
|
|
@@ -48,7 +63,7 @@ module TokenReel
|
|
|
48
63
|
t = t.clamp(0, duration)
|
|
49
64
|
|
|
50
65
|
if t < prompt_done_t
|
|
51
|
-
n = @prompt_interval.positive? ? (t
|
|
66
|
+
n = @prompt_interval.positive? ? tokens_shown(@prompt_offsets, t) : prompt_tokens.size
|
|
52
67
|
State.new(
|
|
53
68
|
phase: :typing_prompt,
|
|
54
69
|
prompt_text: prompt_tokens[0...n].join,
|
|
@@ -69,7 +84,7 @@ module TokenReel
|
|
|
69
84
|
)
|
|
70
85
|
elsif t < reasoning_end_t
|
|
71
86
|
elapsed = t - thinking_end_t
|
|
72
|
-
n = @reasoning_interval.positive? ? (elapsed
|
|
87
|
+
n = @reasoning_interval.positive? ? tokens_shown(@reasoning_offsets, elapsed) : reasoning_tokens.size
|
|
73
88
|
n = n.clamp(0, reasoning_tokens.size)
|
|
74
89
|
State.new(
|
|
75
90
|
phase: :reasoning,
|
|
@@ -81,7 +96,7 @@ module TokenReel
|
|
|
81
96
|
)
|
|
82
97
|
else
|
|
83
98
|
elapsed = t - reasoning_end_t
|
|
84
|
-
n = @response_interval.positive? ? (elapsed
|
|
99
|
+
n = @response_interval.positive? ? tokens_shown(@response_offsets, elapsed) : response_tokens.size
|
|
85
100
|
n = n.clamp(0, response_tokens.size)
|
|
86
101
|
done = n >= response_tokens.size
|
|
87
102
|
State.new(
|
|
@@ -113,6 +128,39 @@ module TokenReel
|
|
|
113
128
|
|
|
114
129
|
private
|
|
115
130
|
|
|
131
|
+
# Builds the cumulative reveal-time array for `count` tokens spaced
|
|
132
|
+
# `interval` seconds apart: offsets[0] == 0, offsets[count] == the
|
|
133
|
+
# total time to reveal them all. With no RNG (variability off) each
|
|
134
|
+
# step is exactly `interval`, matching the un-jittered timeline
|
|
135
|
+
# exactly; with one, each step is independently perturbed.
|
|
136
|
+
def cumulative_offsets(count, interval)
|
|
137
|
+
offsets = [0.0]
|
|
138
|
+
count.times { offsets << offsets.last + jittered_delay(interval) }
|
|
139
|
+
offsets
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def jittered_delay(interval)
|
|
143
|
+
return interval unless @rng
|
|
144
|
+
|
|
145
|
+
pct = JITTER_PCTS.sample(random: @rng)
|
|
146
|
+
z = normal_sample.clamp(-1.0, 1.0) # keep the (rare) normal-curve tail within the chosen bound
|
|
147
|
+
[interval * (1.0 + z * pct), 0.0].max
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Standard normal sample (mean 0, stddev 1) via the Box-Muller transform.
|
|
151
|
+
def normal_sample
|
|
152
|
+
u1 = 1.0 - @rng.rand # (0, 1], avoids log(0)
|
|
153
|
+
u2 = @rng.rand
|
|
154
|
+
Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2 * Math::PI * u2)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Number of tokens fully revealed by `elapsed`, given their
|
|
158
|
+
# cumulative reveal-time array (size == token count + 1).
|
|
159
|
+
def tokens_shown(offsets, elapsed)
|
|
160
|
+
idx = offsets.bsearch_index { |offset| offset > elapsed }
|
|
161
|
+
idx.nil? ? offsets.size - 1 : idx - 1
|
|
162
|
+
end
|
|
163
|
+
|
|
116
164
|
def blink_on?(t)
|
|
117
165
|
(t * (2 * BLINK_HZ)).to_i.even?
|
|
118
166
|
end
|
data/lib/token_reel/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: token_reel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Powell
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -53,6 +52,8 @@ extensions: []
|
|
|
53
52
|
extra_rdoc_files: []
|
|
54
53
|
files:
|
|
55
54
|
- ".github/workflows/ci.yml"
|
|
55
|
+
- ".github/workflows/release.yml"
|
|
56
|
+
- ".github/workflows/version-bump.yml"
|
|
56
57
|
- ".gitignore"
|
|
57
58
|
- Gemfile
|
|
58
59
|
- README.md
|
|
@@ -79,7 +80,6 @@ licenses:
|
|
|
79
80
|
metadata:
|
|
80
81
|
homepage_uri: https://github.com/stringsn88keys/token_reel
|
|
81
82
|
source_code_uri: https://github.com/stringsn88keys/token_reel
|
|
82
|
-
post_install_message:
|
|
83
83
|
rdoc_options: []
|
|
84
84
|
require_paths:
|
|
85
85
|
- lib
|
|
@@ -94,8 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
96
|
requirements: []
|
|
97
|
-
rubygems_version:
|
|
98
|
-
signing_key:
|
|
97
|
+
rubygems_version: 4.0.16
|
|
99
98
|
specification_version: 4
|
|
100
99
|
summary: Render terminal-style GIFs of an LLM prompt/response, streamed at a chosen
|
|
101
100
|
tokens/sec and time-to-first-token
|