-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathindex.js
225 lines (187 loc) · 6.83 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
61
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class ScreenShareSession {
/**
* @type {ImageCapture}
*/
imageCapture = null;
/**
* @type {MediaStream}
*/
stream = null;
/**
* @type {MediaStreamTrack}
*/
videoTrack = null;
/**
* Creates a new Stream object.
* @param {MediaStream} stream Stream object
* @param {ImageCapture} imageCapture ImageCapture object
* @param {MediaStreamTrack} videoTrack Video track object
*/
constructor(stream, imageCapture, videoTrack) {
this.stream = stream;
this.imageCapture = imageCapture;
this.videoTrack = videoTrack;
}
}
/**
* @type {ScreenShareSession}
*/
let session = null;
const { eventSource, event_types } = SillyTavern.getContext();
const canvas = new OffscreenCanvas(window.screen.width, window.screen.height);
const button = createButton();
function updateUI() {
const icon = button.querySelector('i');
const text = button.querySelector('span');
const isSessionActive = !!session;
icon.classList.toggle('fa-desktop', !isSessionActive);
icon.classList.toggle('fa-hand', isSessionActive);
text.innerText = isSessionActive ? 'Stop Screen Share' : 'Screen Share';
}
function createButton() {
const menu = document.getElementById('screen_share_wand_container') ?? document.getElementById('extensionsMenu');
menu.classList.add('interactable');
menu.tabIndex = 0;
const extensionButton = document.createElement('div');
extensionButton.classList.add('list-group-item', 'flex-container', 'flexGap5', 'interactable');
extensionButton.tabIndex = 0;
const icon = document.createElement('i');
icon.classList.add('fa-solid', 'fa-desktop');
const text = document.createElement('span');
text.innerText = 'Screen Share';
extensionButton.appendChild(icon);
extensionButton.appendChild(text);
extensionButton.onclick = handleClick;
async function handleClick() {
if (session) {
session.videoTrack.stop();
session = null;
updateUI();
return console.log('Screen sharing stopped.');
}
await launchScreenShare();
updateUI();
return console.log('Screen sharing started.');
}
if (!menu) {
console.warn('createButton: menu not found');
return extensionButton;
}
menu.appendChild(extensionButton);
return extensionButton;
}
/**
* Generation interceptor for screen sharing.
* @param {object[]} chat Chat messages
* @param {number} _contextSize Context size (unused)
* @param {function} _abort Abort function (unused)
* @param {string} type Type of generation
*/
async function grabFrame(chat, _contextSize, _abort, type) {
if (type === 'quiet') {
console.debug('grabFrame: quiet mode');
return;
}
if (!Array.isArray(chat) || chat.length === 0) {
console.debug('grabFrame: chat is empty');
return;
}
if (!session) {
console.debug('grabFrame: stream is not initialized');
return;
}
if (!session.stream.active) {
console.warn('grabFrame: stream is not active');
return;
}
// We don't want to modify the original message object
// Since it's saved in the chat history
const lastChatMessage = structuredClone(chat[chat.length - 1]);
if (!lastChatMessage) {
console.warn('grabFrame: message is gone??');
return;
}
if (!lastChatMessage.is_user) {
console.debug('grabFrame: message is not from user');
return;
}
if (!lastChatMessage.extra) {
lastChatMessage.extra = {};
}
if (lastChatMessage.extra.image) {
console.debug('grabFrame: image already exists');
return;
}
// Do a little bamboozle to hack the message
chat[chat.length - 1] = lastChatMessage;
// Grab frame
const bitmap = await session.imageCapture.grabFrame();
// Draw frame to canvas
console.debug('launchScreenShare: drawing frame to canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext('2d');
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
// Convert to base64 JPEG string
console.debug('launchScreenShare: converting canvas to base64');
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.95 });
const base64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject();
reader.readAsDataURL(blob);
});
console.log('launchScreenShare: sending frame to chat');
lastChatMessage.extra.image = base64;
}
async function launchScreenShare() {
try {
if (!window.ImageCapture) {
toastr.error('Your browser does not support ImageCapture API. Please use a different browser.');
return;
}
// Get permission to capture the screen
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
if (!stream) {
toastr.error('Failed to start screen sharing. Please try again.');
return;
}
const context = SillyTavern.getContext();
if (context.mainApi !== 'openai') {
toastr.warning('Screen sharing is only supported in Chat Completions.', 'Unsupported API');
return;
}
const imageInliningCheckbox = document.getElementById('openai_image_inlining');
if (imageInliningCheckbox instanceof HTMLInputElement) {
if (!imageInliningCheckbox.checked) {
toastr.warning('Image inlining is turned off. The screen share feature will not work.');
}
}
// Get the video track
const [videoTrack] = stream.getVideoTracks();
if (!videoTrack) {
throw new Error('Failed to get the video track.');
}
// Create an image capture object
const imageCapture = new ImageCapture(videoTrack);
// If the video track is ended, stop the worker
videoTrack.addEventListener('ended', () => {
console.log('launchScreenShare: video ended, stopping session.');
session = null;
updateUI();
});
// If the chat is changed, stop the worker
eventSource.once(event_types.CHAT_CHANGED, () => {
console.log('launchScreenShare: chat changed, stopping session.');
videoTrack.stop();
session = null;
updateUI();
});
// Create a new session object
session = new ScreenShareSession(stream, imageCapture, videoTrack);
} catch (error) {
console.error('Failed to start screen sharing.', error);
toastr.error('Failed to start screen sharing. Check debug console for more details.');
}
}
window['extension_ScreenShare_interceptor'] = grabFrame;