charai 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +169 -0
- data/Rakefile +3 -0
- data/bin/console +8 -0
- data/charai.gemspec +33 -0
- data/lib/charai/action_queue.rb +41 -0
- data/lib/charai/agent.rb +97 -0
- data/lib/charai/browser.rb +142 -0
- data/lib/charai/browser_launcher.rb +170 -0
- data/lib/charai/browser_process.rb +26 -0
- data/lib/charai/browsing_context.rb +201 -0
- data/lib/charai/driver.rb +147 -0
- data/lib/charai/input_tool.rb +393 -0
- data/lib/charai/openai_chat.rb +163 -0
- data/lib/charai/openai_configuration.rb +58 -0
- data/lib/charai/spline_deceleration.rb +129 -0
- data/lib/charai/util.rb +11 -0
- data/lib/charai/version.rb +5 -0
- data/lib/charai/web_socket.rb +156 -0
- data/lib/charai.rb +18 -0
- metadata +109 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
module Charai
|
2
|
+
# ref: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/OverScroller.java
|
3
|
+
# Converted using ChatGPT 4o
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
# deceleration = SplineDeceleration.new(200)
|
7
|
+
# loop do
|
8
|
+
# delta = deceleration.calc
|
9
|
+
# sleep 0.016 # 60fps
|
10
|
+
# break if delta.zero?
|
11
|
+
# end
|
12
|
+
|
13
|
+
class SplineDeceleration
|
14
|
+
DECELERATION_RATE = Math.log(0.78) / Math.log(0.9)
|
15
|
+
INFLEXION = 0.35
|
16
|
+
START_TENSION = 0.5
|
17
|
+
END_TENSION = 1.0
|
18
|
+
P1 = START_TENSION * INFLEXION
|
19
|
+
P2 = 1.0 - END_TENSION * (1.0 - INFLEXION)
|
20
|
+
NB_SAMPLES = 100
|
21
|
+
|
22
|
+
attr_reader :current_velocity, :current_position
|
23
|
+
|
24
|
+
def initialize(initial_velocity)
|
25
|
+
@initial_velocity = initial_velocity
|
26
|
+
@current_velocity = initial_velocity
|
27
|
+
@physical_coeff = 1000
|
28
|
+
@elapsed_time = 0 # 累積時間を管理
|
29
|
+
@duration = calculate_duration
|
30
|
+
@distance = calculate_distance
|
31
|
+
@previous_position = 0 # 前回の位置を保持
|
32
|
+
@current_position = 0
|
33
|
+
|
34
|
+
# スプラインテーブルを構築
|
35
|
+
@spline_position = Array.new(NB_SAMPLES + 1)
|
36
|
+
@spline_time = Array.new(NB_SAMPLES + 1)
|
37
|
+
build_spline_tables
|
38
|
+
end
|
39
|
+
|
40
|
+
def calculate_distance
|
41
|
+
l = Math.log(INFLEXION * @initial_velocity.abs / @physical_coeff)
|
42
|
+
decel_minus_one = DECELERATION_RATE - 1.0
|
43
|
+
@physical_coeff * Math.exp(DECELERATION_RATE / decel_minus_one * l)
|
44
|
+
end
|
45
|
+
|
46
|
+
def calculate_duration
|
47
|
+
l = Math.log(INFLEXION * @initial_velocity.abs / @physical_coeff)
|
48
|
+
decel_minus_one = DECELERATION_RATE - 1.0
|
49
|
+
(1000.0 * Math.exp(l / decel_minus_one)).to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def calc
|
53
|
+
@elapsed_time += 16 # 毎回16ms経過
|
54
|
+
|
55
|
+
return 0 if @elapsed_time > @duration
|
56
|
+
|
57
|
+
t = @elapsed_time.to_f / @duration
|
58
|
+
distance_coef = interpolate_spline_position(t)
|
59
|
+
@current_velocity = interpolate_spline_velocity(t)
|
60
|
+
|
61
|
+
# 現在の位置を計算
|
62
|
+
@current_position = (distance_coef * @distance).to_i
|
63
|
+
|
64
|
+
# 16ms前の位置からのdeltaを計算
|
65
|
+
delta_position = @current_position - @previous_position
|
66
|
+
|
67
|
+
# 現在の位置を次回のために保存
|
68
|
+
@previous_position = @current_position
|
69
|
+
|
70
|
+
delta_position
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def build_spline_tables
|
76
|
+
(0...NB_SAMPLES).each do |i|
|
77
|
+
alpha = i.to_f / NB_SAMPLES
|
78
|
+
# x 方向のスプライン補間
|
79
|
+
x_min = 0.0
|
80
|
+
x_max = 1.0
|
81
|
+
while true
|
82
|
+
x = (x_min + x_max) / 2.0
|
83
|
+
coef = 3.0 * x * (1.0 - x)
|
84
|
+
tx = coef * ((1.0 - x) * P1 + x * P2) + x**3
|
85
|
+
break if (tx - alpha).abs < 1e-5
|
86
|
+
tx > alpha ? x_max = x : x_min = x
|
87
|
+
end
|
88
|
+
@spline_position[i] = coef * ((1.0 - x) * START_TENSION + x) + x**3
|
89
|
+
|
90
|
+
# y 方向のスプライン補間
|
91
|
+
y_min = 0.0
|
92
|
+
y_max = 1.0
|
93
|
+
while true
|
94
|
+
y = (y_min + y_max) / 2.0
|
95
|
+
coef = 3.0 * y * (1.0 - y)
|
96
|
+
dy = coef * ((1.0 - y) * START_TENSION + y) + y**3
|
97
|
+
break if (dy - alpha).abs < 1e-5
|
98
|
+
dy > alpha ? y_max = y : y_min = y
|
99
|
+
end
|
100
|
+
@spline_time[i] = coef * ((1.0 - y) * P1 + y * P2) + y**3
|
101
|
+
end
|
102
|
+
@spline_position[NB_SAMPLES] = @spline_time[NB_SAMPLES] = 1.0
|
103
|
+
end
|
104
|
+
|
105
|
+
def interpolate_spline_position(t)
|
106
|
+
index = (NB_SAMPLES * t).to_i
|
107
|
+
return 1.0 if index >= NB_SAMPLES
|
108
|
+
|
109
|
+
t_inf = index.to_f / NB_SAMPLES
|
110
|
+
t_sup = (index + 1).to_f / NB_SAMPLES
|
111
|
+
d_inf = @spline_position[index]
|
112
|
+
d_sup = @spline_position[index + 1]
|
113
|
+
velocity_coef = (d_sup - d_inf) / (t_sup - t_inf)
|
114
|
+
d_inf + (t - t_inf) * velocity_coef
|
115
|
+
end
|
116
|
+
|
117
|
+
def interpolate_spline_velocity(t)
|
118
|
+
index = (NB_SAMPLES * t).to_i
|
119
|
+
return 0.0 if index >= NB_SAMPLES
|
120
|
+
|
121
|
+
t_inf = index.to_f / NB_SAMPLES
|
122
|
+
t_sup = (index + 1).to_f / NB_SAMPLES
|
123
|
+
d_inf = @spline_position[index]
|
124
|
+
d_sup = @spline_position[index + 1]
|
125
|
+
velocity_coef = (d_sup - d_inf) / (t_sup - t_inf)
|
126
|
+
velocity_coef * @distance / @duration * 1000.0
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/charai/util.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
require 'websocket/driver'
|
4
|
+
|
5
|
+
module Charai
|
6
|
+
# ref: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/client_socket.rb
|
7
|
+
# ref: https://github.com/cavalle/chrome_remote/blob/master/lib/chrome_remote/web_socket_client.rb
|
8
|
+
class WebSocket
|
9
|
+
class DriverImpl # providing #url, #write(string)
|
10
|
+
class SecureSocketFactory
|
11
|
+
def initialize(host, port)
|
12
|
+
@host = host
|
13
|
+
@port = port || 443
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
18
|
+
OpenSSL::SSL::SSLSocket.new(tcp_socket).tap(&:connect)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(url)
|
23
|
+
@url = url
|
24
|
+
|
25
|
+
endpoint = URI.parse(url)
|
26
|
+
@socket =
|
27
|
+
if endpoint.scheme == 'wss'
|
28
|
+
SecureSocketFactory.new(endpoint.host, endpoint.port).create
|
29
|
+
else
|
30
|
+
TCPSocket.new(endpoint.host, endpoint.port)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :url
|
35
|
+
|
36
|
+
def write(data)
|
37
|
+
@socket.write(data)
|
38
|
+
rescue Errno::EPIPE
|
39
|
+
raise EOFError.new('already closed')
|
40
|
+
rescue Errno::ECONNRESET
|
41
|
+
raise EOFError.new('closed by remote')
|
42
|
+
end
|
43
|
+
|
44
|
+
def readpartial(maxlen = 1024)
|
45
|
+
@socket.readpartial(maxlen)
|
46
|
+
rescue Errno::ECONNRESET
|
47
|
+
raise EOFError.new('closed by remote')
|
48
|
+
end
|
49
|
+
|
50
|
+
def dispose
|
51
|
+
@socket.close
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
STATE_CONNECTING = 0
|
56
|
+
STATE_OPENED = 1
|
57
|
+
STATE_CLOSING = 2
|
58
|
+
STATE_CLOSED = 3
|
59
|
+
|
60
|
+
def initialize(url:, max_payload_size: 256 * 1024 * 1024)
|
61
|
+
@impl = DriverImpl.new(url)
|
62
|
+
@driver = ::WebSocket::Driver.client(@impl, max_length: max_payload_size)
|
63
|
+
|
64
|
+
setup
|
65
|
+
@driver.start
|
66
|
+
|
67
|
+
Thread.new do
|
68
|
+
wait_for_data until @ready_state >= STATE_CLOSING
|
69
|
+
rescue EOFError
|
70
|
+
# Browser was gone.
|
71
|
+
# We have nothing todo. Just finish polling.
|
72
|
+
if @ready_state < STATE_CLOSING
|
73
|
+
handle_on_close(reason: 'Going Away', code: 1001)
|
74
|
+
end
|
75
|
+
rescue IOError
|
76
|
+
# connection closed. Just ignore it.
|
77
|
+
raise if @ready_state < STATE_CLOSING
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private def setup
|
82
|
+
@ready_state = STATE_CONNECTING
|
83
|
+
@driver.on(:open) do
|
84
|
+
@ready_state = STATE_OPENED
|
85
|
+
handle_on_open
|
86
|
+
end
|
87
|
+
@driver.on(:close) do |event|
|
88
|
+
@ready_state = STATE_CLOSED
|
89
|
+
handle_on_close(reason: event.reason, code: event.code)
|
90
|
+
end
|
91
|
+
@driver.on(:error) do |event|
|
92
|
+
unless handle_on_error(error_message: event.message)
|
93
|
+
raise event.message
|
94
|
+
end
|
95
|
+
end
|
96
|
+
@driver.on(:message) do |event|
|
97
|
+
handle_on_message(event.data)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private def wait_for_data
|
102
|
+
@driver.parse(@impl.readpartial)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param message [String]
|
106
|
+
def send_text(message)
|
107
|
+
return if @ready_state >= STATE_CLOSING
|
108
|
+
@driver.text(message)
|
109
|
+
end
|
110
|
+
|
111
|
+
def close(code: 1000, reason: "")
|
112
|
+
return if @ready_state >= STATE_CLOSING
|
113
|
+
@ready_state = STATE_CLOSING
|
114
|
+
@driver.close(reason, code)
|
115
|
+
@impl.dispose
|
116
|
+
end
|
117
|
+
|
118
|
+
def on_open(&block)
|
119
|
+
@on_open = block
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param block [Proc(reason: String, code: Numeric)]
|
123
|
+
def on_close(&block)
|
124
|
+
@on_close = block
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param block [Proc(error_message: String)]
|
128
|
+
def on_error(&block)
|
129
|
+
@on_error = block
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_message(&block)
|
133
|
+
@on_message = block
|
134
|
+
end
|
135
|
+
|
136
|
+
private def handle_on_open
|
137
|
+
@on_open&.call
|
138
|
+
end
|
139
|
+
|
140
|
+
private def handle_on_close(reason:, code:)
|
141
|
+
@on_close&.call(reason, code)
|
142
|
+
end
|
143
|
+
|
144
|
+
private def handle_on_error(error_message:)
|
145
|
+
return false if @on_error.nil?
|
146
|
+
|
147
|
+
@on_error.call(error_message)
|
148
|
+
end
|
149
|
+
|
150
|
+
private def handle_on_message(data)
|
151
|
+
return if @ready_state != STATE_OPENED
|
152
|
+
|
153
|
+
@on_message&.call(data)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
data/lib/charai.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'charai/version'
|
2
|
+
|
3
|
+
require 'capybara'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
require 'charai/action_queue'
|
7
|
+
require 'charai/agent'
|
8
|
+
require 'charai/browser'
|
9
|
+
require 'charai/browser_launcher'
|
10
|
+
require 'charai/browser_process'
|
11
|
+
require 'charai/browsing_context'
|
12
|
+
require 'charai/driver'
|
13
|
+
require 'charai/input_tool'
|
14
|
+
require 'charai/openai_chat'
|
15
|
+
require 'charai/openai_configuration'
|
16
|
+
require 'charai/spline_deceleration'
|
17
|
+
require 'charai/util'
|
18
|
+
require 'charai/web_socket'
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: charai
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- YusukeIwaki
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: capybara
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: concurrent-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.1.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.1.6
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: websocket-driver
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.6.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.1
|
55
|
+
description: Prototype impl for Kaigi on Rails 2024 presentation.
|
56
|
+
email:
|
57
|
+
- q7w8e9w8q7w8e9@yahoo.co.jp
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".rspec"
|
63
|
+
- ".rubocop.yml"
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/console
|
69
|
+
- charai.gemspec
|
70
|
+
- lib/charai.rb
|
71
|
+
- lib/charai/action_queue.rb
|
72
|
+
- lib/charai/agent.rb
|
73
|
+
- lib/charai/browser.rb
|
74
|
+
- lib/charai/browser_launcher.rb
|
75
|
+
- lib/charai/browser_process.rb
|
76
|
+
- lib/charai/browsing_context.rb
|
77
|
+
- lib/charai/driver.rb
|
78
|
+
- lib/charai/input_tool.rb
|
79
|
+
- lib/charai/openai_chat.rb
|
80
|
+
- lib/charai/openai_configuration.rb
|
81
|
+
- lib/charai/spline_deceleration.rb
|
82
|
+
- lib/charai/util.rb
|
83
|
+
- lib/charai/version.rb
|
84
|
+
- lib/charai/web_socket.rb
|
85
|
+
homepage: https://github.com/YusukeIwaki/charai
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata:
|
89
|
+
homepage_uri: https://github.com/YusukeIwaki/charai
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 3.2.0
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubygems_version: 3.5.3
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: charai(Chat + Ruby + AI) Capybara driver
|
109
|
+
test_files: []
|