Class: TokenReel::Timeline

Inherits:
Object
  • Object
show all
Defined in:
lib/token_reel/timeline.rb

Overview

Pure function of time -> State. Doesn't touch the filesystem or ImageMagick at all, which makes it cheap to sample as densely as we like when building the frame list.

Constant Summary collapse

2.0
DOT_INTERVAL =

seconds between "thinking..." dot ticks

0.35
JITTER_PCTS =

When config.variability is on, each token's delay is independently jittered by a normally-distributed offset, bounded to +/- one of these percentages (picked at random per token) of its base interval.

[0.10, 0.20, 0.30].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Timeline

Returns a new instance of Timeline.



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
# File 'lib/token_reel/timeline.rb', line 32

def initialize(config)
  @config = config
  @prompt_tokens = Tokenizer.tokenize(config.prompt, config.unit)
  @reasoning_tokens = Tokenizer.tokenize(config.reasoning, config.unit)
  @response_tokens = Tokenizer.tokenize(config.response, config.unit)
  @prompt_full = prompt_tokens.join
  @reasoning_full = reasoning_tokens.join
  @response_full = response_tokens.join

  @prompt_interval = config.prompt_tps.to_f.positive? ? 1.0 / config.prompt_tps : 0
  @reasoning_interval = config.reasoning_tps.to_f.positive? ? 1.0 / config.reasoning_tps : 1.0 / config.tps
  @response_interval = 1.0 / config.tps

  @rng = config.variability ? Random.new(config.seed || Random.new_seed) : nil

  # Cumulative reveal times per token: offsets[n] is the moment the
  # n-th token has fully appeared. With variability off this is just
  # n * interval; with it on, each step is independently jittered,
  # so the array can't be derived by a plain multiply anymore.
  @prompt_offsets = cumulative_offsets(prompt_tokens.size, @prompt_interval)
  @reasoning_offsets = cumulative_offsets(reasoning_tokens.size, @reasoning_interval)
  @response_offsets = cumulative_offsets(response_tokens.size, @response_interval)

  @prompt_done_t = @prompt_offsets.last
  @thinking_end_t = prompt_done_t + config.ttft.to_f
  @reasoning_end_t = thinking_end_t + @reasoning_offsets.last
  @stream_end_t = reasoning_end_t + @response_offsets.last
  @duration = stream_end_t + config.hold.to_f
end

Instance Attribute Details

#durationObject (readonly)

Returns the value of attribute duration.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def duration
  @duration
end

#prompt_done_tObject (readonly)

Returns the value of attribute prompt_done_t.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def prompt_done_t
  @prompt_done_t
end

#prompt_fullObject (readonly)

Returns the value of attribute prompt_full.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def prompt_full
  @prompt_full
end

#prompt_tokensObject (readonly)

Returns the value of attribute prompt_tokens.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def prompt_tokens
  @prompt_tokens
end

#reasoning_end_tObject (readonly)

Returns the value of attribute reasoning_end_t.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def reasoning_end_t
  @reasoning_end_t
end

#reasoning_fullObject (readonly)

Returns the value of attribute reasoning_full.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def reasoning_full
  @reasoning_full
end

#reasoning_tokensObject (readonly)

Returns the value of attribute reasoning_tokens.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def reasoning_tokens
  @reasoning_tokens
end

#response_fullObject (readonly)

Returns the value of attribute response_full.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def response_full
  @response_full
end

#response_tokensObject (readonly)

Returns the value of attribute response_tokens.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def response_tokens
  @response_tokens
end

#stream_end_tObject (readonly)

Returns the value of attribute stream_end_t.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def stream_end_t
  @stream_end_t
end

#thinking_end_tObject (readonly)

Returns the value of attribute thinking_end_t.



28
29
30
# File 'lib/token_reel/timeline.rb', line 28

def thinking_end_t
  @thinking_end_t
end

Instance Method Details

#final_stateObject

The fully-revealed, cursor-off state -- used to size the canvas so every frame in the GIF shares identical dimensions.



115
116
117
118
# File 'lib/token_reel/timeline.rb', line 115

def final_state
  State.new(phase: :done, prompt_text: prompt_full, reasoning_text: "", response_text: response_full,
             cursor_on: false, dot_count: 0)
end

#max_reasoning_stateObject

The reasoning phase in full, cursor off -- used alongside final_state to size the canvas, since the reasoning trace (shown only while streaming, then replaced by the response) can be taller than the finished response.



124
125
126
127
# File 'lib/token_reel/timeline.rb', line 124

def max_reasoning_state
  State.new(phase: :reasoning, prompt_text: prompt_full, reasoning_text: reasoning_full, response_text: "",
             cursor_on: false, dot_count: 0)
end

#state_at(t) ⇒ Object



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
# File 'lib/token_reel/timeline.rb', line 62

def state_at(t)
  t = t.clamp(0, duration)

  if t < prompt_done_t
    n = @prompt_interval.positive? ? tokens_shown(@prompt_offsets, t) : prompt_tokens.size
    State.new(
      phase: :typing_prompt,
      prompt_text: prompt_tokens[0...n].join,
      reasoning_text: "",
      response_text: "",
      cursor_on: blink_on?(t),
      dot_count: 0
    )
  elsif t < thinking_end_t
    elapsed = t - prompt_done_t
    State.new(
      phase: :thinking,
      prompt_text: prompt_full,
      reasoning_text: "",
      response_text: "",
      cursor_on: blink_on?(t),
      dot_count: ((elapsed / DOT_INTERVAL).to_i % 4)
    )
  elsif t < reasoning_end_t
    elapsed = t - thinking_end_t
    n = @reasoning_interval.positive? ? tokens_shown(@reasoning_offsets, elapsed) : reasoning_tokens.size
    n = n.clamp(0, reasoning_tokens.size)
    State.new(
      phase: :reasoning,
      prompt_text: prompt_full,
      reasoning_text: reasoning_tokens[0...n].join,
      response_text: "",
      cursor_on: blink_on?(t),
      dot_count: 0
    )
  else
    elapsed = t - reasoning_end_t
    n = @response_interval.positive? ? tokens_shown(@response_offsets, elapsed) : response_tokens.size
    n = n.clamp(0, response_tokens.size)
    done = n >= response_tokens.size
    State.new(
      phase: done ? :done : :streaming,
      prompt_text: prompt_full,
      reasoning_text: "",
      response_text: response_tokens[0...n].join,
      cursor_on: blink_on?(t),
      dot_count: 0
    )
  end
end