Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Update index.html
Browse files- index.html +251 -293
index.html
CHANGED
|
@@ -709,19 +709,17 @@
|
|
| 709 |
const app = {
|
| 710 |
mode: 'text',
|
| 711 |
isGenerating: false,
|
| 712 |
-
isConnected: false,
|
| 713 |
frameCount: 0,
|
| 714 |
maxFrames: 234,
|
| 715 |
ws: null,
|
| 716 |
frameBuffer: [],
|
| 717 |
playbackInterval: null,
|
| 718 |
frameExtractionInterval: null,
|
| 719 |
-
mediaRecorder: null,
|
| 720 |
-
recordedChunks: [],
|
| 721 |
-
sessionStarted: false,
|
| 722 |
webcamStream: null,
|
| 723 |
-
videoMetadata: null,
|
| 724 |
currentVideoFile: null,
|
|
|
|
|
|
|
|
|
|
| 725 |
promptUpdateTimer: null,
|
| 726 |
pendingPromptUpdate: null,
|
| 727 |
|
|
@@ -735,6 +733,9 @@
|
|
| 735 |
document.getElementById('videoModeBtn').addEventListener('click', () => this.setMode('video'));
|
| 736 |
document.getElementById('webcamModeBtn').addEventListener('click', () => this.setMode('webcam'));
|
| 737 |
|
|
|
|
|
|
|
|
|
|
| 738 |
document.getElementById('playbackFps').addEventListener('input', (e) => {
|
| 739 |
document.getElementById('playbackFpsValue').textContent = e.target.value + ' fps';
|
| 740 |
if (this.playbackInterval) {
|
|
@@ -761,16 +762,13 @@
|
|
| 761 |
this.queuePromptUpdate();
|
| 762 |
}
|
| 763 |
});
|
| 764 |
-
|
| 765 |
-
// Prompt changes
|
| 766 |
document.getElementById('prompt').addEventListener('input', () => {
|
| 767 |
if (this.isGenerating && (this.mode === 'text' || this.mode === 'webcam')) {
|
| 768 |
this.queuePromptUpdate();
|
| 769 |
}
|
| 770 |
});
|
| 771 |
-
|
| 772 |
-
// Video file upload
|
| 773 |
-
document.getElementById('videoFile').addEventListener('change', (e) => this.handleVideoUpload(e));
|
| 774 |
},
|
| 775 |
|
| 776 |
setMode(mode) {
|
|
@@ -829,29 +827,216 @@
|
|
| 829 |
document.getElementById('maxFrames').textContent = estimatedFrames;
|
| 830 |
},
|
| 831 |
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
prompt: document.getElementById('prompt').value,
|
| 836 |
-
num_blocks: parseInt(document.getElementById('numBlocks').value)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
-
|
| 845 |
-
this.
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
},
|
| 856 |
|
| 857 |
async handleVideoUpload(event) {
|
|
@@ -893,10 +1078,7 @@
|
|
| 893 |
const video = document.getElementById('hiddenVideo');
|
| 894 |
const canvas = document.getElementById('extractionCanvas');
|
| 895 |
|
| 896 |
-
if (!video || !canvas)
|
| 897 |
-
console.error('Video or canvas element not found');
|
| 898 |
-
return null;
|
| 899 |
-
}
|
| 900 |
|
| 901 |
const ctx = canvas.getContext('2d');
|
| 902 |
canvas.width = 832;
|
|
@@ -915,7 +1097,6 @@
|
|
| 915 |
return new Promise((resolve) => {
|
| 916 |
canvas.toBlob(async (blob) => {
|
| 917 |
if (!blob) {
|
| 918 |
-
console.error('Failed to create blob from canvas');
|
| 919 |
resolve(null);
|
| 920 |
return;
|
| 921 |
}
|
|
@@ -924,7 +1105,7 @@
|
|
| 924 |
const uint8Array = new Uint8Array(arrayBuffer);
|
| 925 |
resolve(uint8Array);
|
| 926 |
} catch (error) {
|
| 927 |
-
console.error('Failed to convert blob
|
| 928 |
resolve(null);
|
| 929 |
}
|
| 930 |
}, 'image/jpeg', 0.9);
|
|
@@ -935,50 +1116,38 @@
|
|
| 935 |
const video = document.getElementById('webcamVideo');
|
| 936 |
const canvas = document.getElementById('extractionCanvas');
|
| 937 |
|
| 938 |
-
if (!video || !canvas || !video.videoWidth)
|
| 939 |
-
console.error('Webcam not ready');
|
| 940 |
-
return null;
|
| 941 |
-
}
|
| 942 |
|
| 943 |
const ctx = canvas.getContext('2d');
|
| 944 |
-
|
| 945 |
-
// Set fixed dimensions
|
| 946 |
const targetWidth = 832;
|
| 947 |
const targetHeight = 480;
|
| 948 |
canvas.width = targetWidth;
|
| 949 |
canvas.height = targetHeight;
|
| 950 |
|
| 951 |
-
// Calculate source rectangle to crop center 832x480 from webcam
|
| 952 |
const videoWidth = video.videoWidth;
|
| 953 |
const videoHeight = video.videoHeight;
|
| 954 |
-
|
| 955 |
-
// Calculate crop dimensions maintaining aspect ratio
|
| 956 |
const sourceAspect = videoWidth / videoHeight;
|
| 957 |
const targetAspect = targetWidth / targetHeight;
|
| 958 |
|
| 959 |
let sx, sy, sWidth, sHeight;
|
| 960 |
|
| 961 |
if (sourceAspect > targetAspect) {
|
| 962 |
-
// Video is wider, crop sides
|
| 963 |
sHeight = videoHeight;
|
| 964 |
sWidth = videoHeight * targetAspect;
|
| 965 |
sx = (videoWidth - sWidth) / 2;
|
| 966 |
sy = 0;
|
| 967 |
} else {
|
| 968 |
-
// Video is taller, crop top/bottom
|
| 969 |
sWidth = videoWidth;
|
| 970 |
sHeight = videoWidth / targetAspect;
|
| 971 |
sx = 0;
|
| 972 |
sy = (videoHeight - sHeight) / 2;
|
| 973 |
}
|
| 974 |
|
| 975 |
-
// Draw cropped and scaled image
|
| 976 |
ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, targetWidth, targetHeight);
|
| 977 |
|
| 978 |
return new Promise((resolve) => {
|
| 979 |
canvas.toBlob(async (blob) => {
|
| 980 |
if (!blob) {
|
| 981 |
-
console.error('Failed to create blob from canvas');
|
| 982 |
resolve(null);
|
| 983 |
return;
|
| 984 |
}
|
|
@@ -987,7 +1156,7 @@
|
|
| 987 |
const uint8Array = new Uint8Array(arrayBuffer);
|
| 988 |
resolve(uint8Array);
|
| 989 |
} catch (error) {
|
| 990 |
-
console.error('Failed to convert blob
|
| 991 |
resolve(null);
|
| 992 |
}
|
| 993 |
}, 'image/jpeg', 0.9);
|
|
@@ -1026,28 +1195,18 @@
|
|
| 1026 |
}
|
| 1027 |
|
| 1028 |
const strengthValue = parseFloat(document.getElementById('strength').value);
|
| 1029 |
-
if (isNaN(strengthValue)) {
|
| 1030 |
-
console.error('Invalid strength value');
|
| 1031 |
-
return;
|
| 1032 |
-
}
|
| 1033 |
-
|
| 1034 |
const message = {
|
| 1035 |
image: frameBytes,
|
| 1036 |
strength: strengthValue,
|
| 1037 |
timestamp: Date.now()
|
| 1038 |
};
|
| 1039 |
|
| 1040 |
-
// For webcam mode, include prompt and num_blocks
|
| 1041 |
if (this.mode === 'webcam') {
|
| 1042 |
message.prompt = document.getElementById('prompt').value;
|
| 1043 |
message.num_blocks = parseInt(document.getElementById('numBlocks').value);
|
| 1044 |
}
|
| 1045 |
|
| 1046 |
-
if (!(message.image instanceof Uint8Array)) {
|
| 1047 |
-
console.error('Frame bytes is not a Uint8Array');
|
| 1048 |
-
return;
|
| 1049 |
-
}
|
| 1050 |
-
|
| 1051 |
const encoded = createMsgpackEncoder(message);
|
| 1052 |
this.ws.send(encoded);
|
| 1053 |
}, intervalMs);
|
|
@@ -1064,189 +1223,25 @@
|
|
| 1064 |
video.currentTime = 0;
|
| 1065 |
},
|
| 1066 |
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
},
|
| 1070 |
-
|
| 1071 |
-
async toggleGeneration() {
|
| 1072 |
-
if (this.isGenerating) {
|
| 1073 |
-
this.disconnect();
|
| 1074 |
-
} else {
|
| 1075 |
-
// Record session start
|
| 1076 |
-
if (!this.sessionStarted) {
|
| 1077 |
-
try {
|
| 1078 |
-
const response = await fetch('/api/start-session', {method: 'POST'});
|
| 1079 |
-
if (!response.ok) {
|
| 1080 |
-
const data = await response.json();
|
| 1081 |
-
this.showError(data.detail || 'Failed to start session');
|
| 1082 |
-
return;
|
| 1083 |
-
}
|
| 1084 |
-
this.sessionStarted = true;
|
| 1085 |
-
} catch (error) {
|
| 1086 |
-
this.showError('Failed to start session');
|
| 1087 |
-
return;
|
| 1088 |
-
}
|
| 1089 |
-
}
|
| 1090 |
-
|
| 1091 |
-
const prompt = document.getElementById('prompt').value.trim();
|
| 1092 |
-
if (!prompt) {
|
| 1093 |
-
this.showError('Please enter a prompt');
|
| 1094 |
-
return;
|
| 1095 |
-
}
|
| 1096 |
-
|
| 1097 |
-
this.isGenerating = true;
|
| 1098 |
-
this.updateUI();
|
| 1099 |
-
|
| 1100 |
-
// Connect to backend WebSocket proxy (keeps API key secure)
|
| 1101 |
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 1102 |
-
const wsUrl = `${protocol}//${window.location.host}/ws/video-gen`;
|
| 1103 |
-
|
| 1104 |
-
try {
|
| 1105 |
-
this.ws = new WebSocket(wsUrl);
|
| 1106 |
-
this.ws.binaryType = 'arraybuffer';
|
| 1107 |
-
|
| 1108 |
-
this.ws.onopen = () => {
|
| 1109 |
-
this.showInfo('Connected! Waiting for ready signal...');
|
| 1110 |
-
};
|
| 1111 |
-
|
| 1112 |
-
this.ws.onmessage = async (event) => {
|
| 1113 |
-
if (typeof event.data === 'string') {
|
| 1114 |
-
try {
|
| 1115 |
-
const data = JSON.parse(event.data);
|
| 1116 |
-
if (data.status === 'ready') {
|
| 1117 |
-
this.showInfo('Ready - sending parameters...');
|
| 1118 |
-
await this.sendInitialParams();
|
| 1119 |
-
} else if (data.error) {
|
| 1120 |
-
this.showError(`Server error: ${JSON.stringify(data.error)}`);
|
| 1121 |
-
this.disconnect();
|
| 1122 |
-
}
|
| 1123 |
-
} catch (e) {
|
| 1124 |
-
console.log('Text message:', event.data);
|
| 1125 |
-
}
|
| 1126 |
-
} else if (event.data instanceof ArrayBuffer) {
|
| 1127 |
-
await this.displayFrame(event.data);
|
| 1128 |
-
}
|
| 1129 |
-
};
|
| 1130 |
-
|
| 1131 |
-
this.ws.onerror = (error) => {
|
| 1132 |
-
this.showError('WebSocket connection error');
|
| 1133 |
-
console.error('WebSocket error:', error);
|
| 1134 |
-
};
|
| 1135 |
-
|
| 1136 |
-
this.ws.onclose = (event) => {
|
| 1137 |
-
this.showInfo(`Disconnected: ${event.reason || 'Connection closed'}`);
|
| 1138 |
-
this.isGenerating = false;
|
| 1139 |
-
this.updateUI();
|
| 1140 |
-
};
|
| 1141 |
-
|
| 1142 |
-
} catch (error) {
|
| 1143 |
-
this.showError('Failed to connect: ' + error.message);
|
| 1144 |
-
this.isGenerating = false;
|
| 1145 |
-
this.updateUI();
|
| 1146 |
-
}
|
| 1147 |
-
}
|
| 1148 |
-
},
|
| 1149 |
-
|
| 1150 |
-
async sendInitialParams() {
|
| 1151 |
-
const payload = {
|
| 1152 |
prompt: document.getElementById('prompt').value,
|
| 1153 |
-
num_blocks: parseInt(document.getElementById('numBlocks').value)
|
| 1154 |
-
num_denoising_steps: 4,
|
| 1155 |
-
strength: parseFloat(document.getElementById('strength').value) || 0.45,
|
| 1156 |
-
width: 832,
|
| 1157 |
-
height: 480
|
| 1158 |
};
|
| 1159 |
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
try {
|
| 1163 |
-
const video = document.getElementById('hiddenVideo');
|
| 1164 |
-
if (!video) {
|
| 1165 |
-
throw new Error('Video element not found');
|
| 1166 |
-
}
|
| 1167 |
-
|
| 1168 |
-
video.currentTime = 0;
|
| 1169 |
-
|
| 1170 |
-
await new Promise((resolve, reject) => {
|
| 1171 |
-
const timeout = setTimeout(() => {
|
| 1172 |
-
reject(new Error('Video seek timeout'));
|
| 1173 |
-
}, 5000);
|
| 1174 |
-
|
| 1175 |
-
video.onseeked = () => {
|
| 1176 |
-
clearTimeout(timeout);
|
| 1177 |
-
resolve();
|
| 1178 |
-
};
|
| 1179 |
-
});
|
| 1180 |
-
|
| 1181 |
-
const startFrame = await this.extractVideoFrameBytes();
|
| 1182 |
-
if (!startFrame || !(startFrame instanceof Uint8Array)) {
|
| 1183 |
-
throw new Error('Failed to extract valid start frame');
|
| 1184 |
-
}
|
| 1185 |
-
|
| 1186 |
-
payload.start_frame = new Uint8Array(startFrame);
|
| 1187 |
-
console.log('Start frame extracted:', payload.start_frame.length, 'bytes');
|
| 1188 |
-
} catch (error) {
|
| 1189 |
-
this.showError(`Failed to extract start frame: ${error.message}`);
|
| 1190 |
-
return false;
|
| 1191 |
-
}
|
| 1192 |
-
} else if (this.mode === 'webcam') {
|
| 1193 |
-
try {
|
| 1194 |
-
const startFrame = await this.extractWebcamBytes();
|
| 1195 |
-
if (!startFrame || !(startFrame instanceof Uint8Array)) {
|
| 1196 |
-
throw new Error('Failed to extract valid start frame from webcam');
|
| 1197 |
-
}
|
| 1198 |
-
payload.start_frame = new Uint8Array(startFrame);
|
| 1199 |
-
console.log('Start frame extracted from webcam:', payload.start_frame.length, 'bytes');
|
| 1200 |
-
} catch (error) {
|
| 1201 |
-
this.showError(`Failed to extract start frame from webcam: ${error.message}`);
|
| 1202 |
-
return false;
|
| 1203 |
-
}
|
| 1204 |
-
}
|
| 1205 |
-
|
| 1206 |
-
const seedValue = document.getElementById('seed').value;
|
| 1207 |
-
if (seedValue && seedValue.trim() !== '') {
|
| 1208 |
-
payload.seed = parseInt(seedValue);
|
| 1209 |
-
} else {
|
| 1210 |
-
payload.seed = Math.floor(Math.random() * (1 << 24));
|
| 1211 |
}
|
| 1212 |
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
this.frameBuffer = [];
|
| 1221 |
-
this.frameCount = 0;
|
| 1222 |
-
document.getElementById('frameCount').textContent = '0';
|
| 1223 |
-
this.updateProgress();
|
| 1224 |
-
|
| 1225 |
-
this.startRecording();
|
| 1226 |
-
|
| 1227 |
-
try {
|
| 1228 |
-
console.log('Sending payload:', {
|
| 1229 |
-
...payload,
|
| 1230 |
-
start_frame: payload.start_frame ? `[${payload.start_frame.length} bytes]` : undefined
|
| 1231 |
-
});
|
| 1232 |
-
|
| 1233 |
-
const encoded = createMsgpackEncoder(payload);
|
| 1234 |
-
this.ws.send(encoded);
|
| 1235 |
-
this.showInfo('Generation started!');
|
| 1236 |
-
this.isConnected = true;
|
| 1237 |
-
document.getElementById('statusPill').className = 'status-pill status-connected';
|
| 1238 |
-
document.getElementById('statusPill').textContent = 'Connected';
|
| 1239 |
-
|
| 1240 |
-
// Start frame extraction for video/webcam modes
|
| 1241 |
-
if (this.mode === 'video' || this.mode === 'webcam') {
|
| 1242 |
-
setTimeout(() => this.startFrameExtraction(), 500);
|
| 1243 |
}
|
| 1244 |
-
|
| 1245 |
-
return true;
|
| 1246 |
-
} catch (error) {
|
| 1247 |
-
this.showError('Failed to send parameters: ' + error.message);
|
| 1248 |
-
return false;
|
| 1249 |
-
}
|
| 1250 |
},
|
| 1251 |
|
| 1252 |
startRecording() {
|
|
@@ -1276,62 +1271,6 @@
|
|
| 1276 |
}
|
| 1277 |
},
|
| 1278 |
|
| 1279 |
-
updateProgress() {
|
| 1280 |
-
const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
|
| 1281 |
-
document.getElementById('progressFill').style.width = progress + '%';
|
| 1282 |
-
|
| 1283 |
-
// Auto-disconnect when reaching max frames
|
| 1284 |
-
if (this.frameCount >= this.maxFrames && this.isGenerating) {
|
| 1285 |
-
this.showInfo(`Reached maximum frames (${this.maxFrames}). Disconnecting...`);
|
| 1286 |
-
setTimeout(() => this.disconnect(), 1000);
|
| 1287 |
-
}
|
| 1288 |
-
},
|
| 1289 |
-
|
| 1290 |
-
async displayFrame(imageData) {
|
| 1291 |
-
const blob = new Blob([imageData], { type: 'image/jpeg' });
|
| 1292 |
-
const bitmap = await createImageBitmap(blob);
|
| 1293 |
-
|
| 1294 |
-
this.frameBuffer.push(bitmap);
|
| 1295 |
-
this.frameCount++;
|
| 1296 |
-
document.getElementById('frameCount').textContent = this.frameCount;
|
| 1297 |
-
|
| 1298 |
-
if (this.frameCount === 1) {
|
| 1299 |
-
document.getElementById('placeholder').style.display = 'none';
|
| 1300 |
-
this.startPlaybackLoop();
|
| 1301 |
-
}
|
| 1302 |
-
|
| 1303 |
-
// Update progress
|
| 1304 |
-
const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
|
| 1305 |
-
document.getElementById('progressFill').style.width = progress + '%';
|
| 1306 |
-
},
|
| 1307 |
-
|
| 1308 |
-
drawNextFrame() {
|
| 1309 |
-
if (this.frameBuffer.length === 0) return;
|
| 1310 |
-
|
| 1311 |
-
const canvas = document.getElementById('outputCanvas');
|
| 1312 |
-
const bitmap = this.frameBuffer.shift();
|
| 1313 |
-
|
| 1314 |
-
canvas.width = bitmap.width;
|
| 1315 |
-
canvas.height = bitmap.height;
|
| 1316 |
-
|
| 1317 |
-
const ctx = canvas.getContext('2d');
|
| 1318 |
-
ctx.drawImage(bitmap, 0, 0);
|
| 1319 |
-
|
| 1320 |
-
if (typeof bitmap.close === 'function') {
|
| 1321 |
-
bitmap.close();
|
| 1322 |
-
}
|
| 1323 |
-
},
|
| 1324 |
-
|
| 1325 |
-
startPlaybackLoop() {
|
| 1326 |
-
if (this.playbackInterval) {
|
| 1327 |
-
clearInterval(this.playbackInterval);
|
| 1328 |
-
}
|
| 1329 |
-
|
| 1330 |
-
const fps = parseInt(document.getElementById('playbackFps').value);
|
| 1331 |
-
const intervalMs = Math.max(10, Math.floor(1000 / fps));
|
| 1332 |
-
this.playbackInterval = setInterval(() => this.drawNextFrame(), intervalMs);
|
| 1333 |
-
},
|
| 1334 |
-
|
| 1335 |
disconnect() {
|
| 1336 |
if (this.ws) {
|
| 1337 |
this.ws.close();
|
|
@@ -1341,13 +1280,32 @@
|
|
| 1341 |
clearInterval(this.playbackInterval);
|
| 1342 |
this.playbackInterval = null;
|
| 1343 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1344 |
this.stopWebcam();
|
|
|
|
| 1345 |
this.isGenerating = false;
|
| 1346 |
this.updateUI();
|
| 1347 |
},
|
| 1348 |
|
| 1349 |
downloadVideo() {
|
| 1350 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1351 |
},
|
| 1352 |
|
| 1353 |
updateUI() {
|
|
|
|
| 709 |
const app = {
|
| 710 |
mode: 'text',
|
| 711 |
isGenerating: false,
|
|
|
|
| 712 |
frameCount: 0,
|
| 713 |
maxFrames: 234,
|
| 714 |
ws: null,
|
| 715 |
frameBuffer: [],
|
| 716 |
playbackInterval: null,
|
| 717 |
frameExtractionInterval: null,
|
|
|
|
|
|
|
|
|
|
| 718 |
webcamStream: null,
|
|
|
|
| 719 |
currentVideoFile: null,
|
| 720 |
+
videoMetadata: null,
|
| 721 |
+
mediaRecorder: null,
|
| 722 |
+
recordedChunks: [],
|
| 723 |
promptUpdateTimer: null,
|
| 724 |
pendingPromptUpdate: null,
|
| 725 |
|
|
|
|
| 733 |
document.getElementById('videoModeBtn').addEventListener('click', () => this.setMode('video'));
|
| 734 |
document.getElementById('webcamModeBtn').addEventListener('click', () => this.setMode('webcam'));
|
| 735 |
|
| 736 |
+
// Video file upload
|
| 737 |
+
document.getElementById('videoFile').addEventListener('change', (e) => this.handleVideoUpload(e));
|
| 738 |
+
|
| 739 |
document.getElementById('playbackFps').addEventListener('input', (e) => {
|
| 740 |
document.getElementById('playbackFpsValue').textContent = e.target.value + ' fps';
|
| 741 |
if (this.playbackInterval) {
|
|
|
|
| 762 |
this.queuePromptUpdate();
|
| 763 |
}
|
| 764 |
});
|
| 765 |
+
|
| 766 |
+
// Prompt changes for live updates
|
| 767 |
document.getElementById('prompt').addEventListener('input', () => {
|
| 768 |
if (this.isGenerating && (this.mode === 'text' || this.mode === 'webcam')) {
|
| 769 |
this.queuePromptUpdate();
|
| 770 |
}
|
| 771 |
});
|
|
|
|
|
|
|
|
|
|
| 772 |
},
|
| 773 |
|
| 774 |
setMode(mode) {
|
|
|
|
| 827 |
document.getElementById('maxFrames').textContent = estimatedFrames;
|
| 828 |
},
|
| 829 |
|
| 830 |
+
randomizeSeed() {
|
| 831 |
+
document.getElementById('seed').value = Math.floor(Math.random() * (1 << 24));
|
| 832 |
+
},
|
| 833 |
+
|
| 834 |
+
async toggleGeneration() {
|
| 835 |
+
if (this.isGenerating) {
|
| 836 |
+
this.disconnect();
|
| 837 |
+
} else {
|
| 838 |
+
// ✅ Record session EVERY time user clicks "Start Generation"
|
| 839 |
+
try {
|
| 840 |
+
const response = await fetch('/api/start-session', {method: 'POST'});
|
| 841 |
+
if (!response.ok) {
|
| 842 |
+
const data = await response.json();
|
| 843 |
+
this.showError(data.detail || 'Failed to start session');
|
| 844 |
+
return;
|
| 845 |
+
}
|
| 846 |
+
// Update session count in UI
|
| 847 |
+
const sessionData = await response.json();
|
| 848 |
+
const usedSpan = document.querySelector('.usage-info');
|
| 849 |
+
if (usedSpan) {
|
| 850 |
+
usedSpan.textContent = `Sessions: ${sessionData.sessions_used}/${sessionData.sessions_limit} today`;
|
| 851 |
+
}
|
| 852 |
+
} catch (error) {
|
| 853 |
+
this.showError('Failed to start session');
|
| 854 |
+
return;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
const prompt = document.getElementById('prompt').value.trim();
|
| 858 |
+
if (!prompt) {
|
| 859 |
+
this.showError('Please enter a prompt');
|
| 860 |
+
return;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
if (this.mode === 'video' && !this.currentVideoFile) {
|
| 864 |
+
this.showError('Please upload a video file');
|
| 865 |
+
return;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
if (this.mode === 'webcam' && !this.webcamStream) {
|
| 869 |
+
this.showError('Webcam not started');
|
| 870 |
+
return;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
this.isGenerating = true;
|
| 874 |
+
this.frameCount = 0;
|
| 875 |
+
document.getElementById('frameCount').textContent = '0';
|
| 876 |
+
this.frameBuffer = [];
|
| 877 |
+
this.updateUI();
|
| 878 |
+
|
| 879 |
+
// Start recording
|
| 880 |
+
this.startRecording();
|
| 881 |
+
|
| 882 |
+
// Connect to backend WebSocket proxy
|
| 883 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 884 |
+
const wsUrl = `${protocol}//${window.location.host}/ws/video-gen`;
|
| 885 |
+
|
| 886 |
+
try {
|
| 887 |
+
this.ws = new WebSocket(wsUrl);
|
| 888 |
+
this.ws.binaryType = 'arraybuffer';
|
| 889 |
+
|
| 890 |
+
this.ws.onopen = () => {
|
| 891 |
+
this.showInfo('Connected! Waiting for ready signal...');
|
| 892 |
+
};
|
| 893 |
+
|
| 894 |
+
this.ws.onmessage = async (event) => {
|
| 895 |
+
if (typeof event.data === 'string') {
|
| 896 |
+
try {
|
| 897 |
+
const data = JSON.parse(event.data);
|
| 898 |
+
if (data.status === 'ready') {
|
| 899 |
+
this.showInfo('Ready - sending parameters...');
|
| 900 |
+
await this.sendInitialParams();
|
| 901 |
+
} else if (data.error) {
|
| 902 |
+
this.showError(`Server error: ${JSON.stringify(data.error)}`);
|
| 903 |
+
this.disconnect();
|
| 904 |
+
}
|
| 905 |
+
} catch (e) {
|
| 906 |
+
console.log('Text message:', event.data);
|
| 907 |
+
}
|
| 908 |
+
} else if (event.data instanceof ArrayBuffer) {
|
| 909 |
+
await this.displayFrame(event.data);
|
| 910 |
+
}
|
| 911 |
+
};
|
| 912 |
+
|
| 913 |
+
this.ws.onerror = (error) => {
|
| 914 |
+
this.showError('WebSocket connection error');
|
| 915 |
+
console.error('WebSocket error:', error);
|
| 916 |
+
};
|
| 917 |
+
|
| 918 |
+
this.ws.onclose = (event) => {
|
| 919 |
+
this.showInfo(`Disconnected: ${event.reason || 'Connection closed'}`);
|
| 920 |
+
this.isGenerating = false;
|
| 921 |
+
this.stopFrameExtraction();
|
| 922 |
+
this.stopRecording();
|
| 923 |
+
this.updateUI();
|
| 924 |
+
};
|
| 925 |
+
|
| 926 |
+
} catch (error) {
|
| 927 |
+
this.showError('Failed to connect: ' + error.message);
|
| 928 |
+
this.isGenerating = false;
|
| 929 |
+
this.updateUI();
|
| 930 |
+
}
|
| 931 |
+
}
|
| 932 |
+
},
|
| 933 |
+
|
| 934 |
+
async sendInitialParams() {
|
| 935 |
+
const payload = {
|
| 936 |
prompt: document.getElementById('prompt').value,
|
| 937 |
+
num_blocks: parseInt(document.getElementById('numBlocks').value),
|
| 938 |
+
num_denoising_steps: 4,
|
| 939 |
+
strength: parseFloat(document.getElementById('strength').value) || 0.45,
|
| 940 |
+
width: 832,
|
| 941 |
+
height: 480
|
| 942 |
};
|
| 943 |
+
|
| 944 |
+
// Add start_frame for video and webcam modes
|
| 945 |
+
if (this.mode === 'video' && this.currentVideoFile) {
|
| 946 |
+
try {
|
| 947 |
+
const video = document.getElementById('hiddenVideo');
|
| 948 |
+
video.currentTime = 0;
|
| 949 |
+
await new Promise((resolve, reject) => {
|
| 950 |
+
const timeout = setTimeout(() => reject(new Error('Video seek timeout')), 5000);
|
| 951 |
+
video.onseeked = () => {
|
| 952 |
+
clearTimeout(timeout);
|
| 953 |
+
resolve();
|
| 954 |
+
};
|
| 955 |
+
});
|
| 956 |
+
const startFrame = await this.extractVideoFrameBytes();
|
| 957 |
+
if (!startFrame) throw new Error('Failed to extract start frame');
|
| 958 |
+
payload.start_frame = startFrame;
|
| 959 |
+
} catch (error) {
|
| 960 |
+
this.showError(`Failed to extract start frame: ${error.message}`);
|
| 961 |
+
this.disconnect();
|
| 962 |
+
return;
|
| 963 |
+
}
|
| 964 |
+
} else if (this.mode === 'webcam') {
|
| 965 |
+
try {
|
| 966 |
+
const startFrame = await this.extractWebcamBytes();
|
| 967 |
+
if (!startFrame) throw new Error('Failed to extract webcam frame');
|
| 968 |
+
payload.start_frame = startFrame;
|
| 969 |
+
} catch (error) {
|
| 970 |
+
this.showError(`Failed to extract webcam frame: ${error.message}`);
|
| 971 |
+
this.disconnect();
|
| 972 |
+
return;
|
| 973 |
+
}
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
const seedValue = document.getElementById('seed').value;
|
| 977 |
+
if (seedValue && seedValue.trim() !== '') {
|
| 978 |
+
payload.seed = parseInt(seedValue);
|
| 979 |
+
} else {
|
| 980 |
+
payload.seed = Math.floor(Math.random() * (1 << 24));
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
try {
|
| 984 |
+
const encoded = createMsgpackEncoder(payload);
|
| 985 |
+
this.ws.send(encoded);
|
| 986 |
+
this.showInfo('Generation started!');
|
| 987 |
+
|
| 988 |
+
// Start frame extraction for v2v and webcam modes
|
| 989 |
+
if (this.mode === 'video' || this.mode === 'webcam') {
|
| 990 |
+
setTimeout(() => this.startFrameExtraction(), 500);
|
| 991 |
+
}
|
| 992 |
+
} catch (error) {
|
| 993 |
+
this.showError('Failed to send parameters: ' + error.message);
|
| 994 |
+
}
|
| 995 |
+
},
|
| 996 |
|
| 997 |
+
async displayFrame(imageData) {
|
| 998 |
+
const blob = new Blob([imageData], { type: 'image/jpeg' });
|
| 999 |
+
const bitmap = await createImageBitmap(blob);
|
| 1000 |
+
|
| 1001 |
+
this.frameBuffer.push(bitmap);
|
| 1002 |
+
this.frameCount++;
|
| 1003 |
+
document.getElementById('frameCount').textContent = this.frameCount;
|
| 1004 |
+
|
| 1005 |
+
if (this.frameCount === 1) {
|
| 1006 |
+
document.getElementById('placeholder').style.display = 'none';
|
| 1007 |
+
this.startPlaybackLoop();
|
| 1008 |
}
|
| 1009 |
+
|
| 1010 |
+
// Update progress
|
| 1011 |
+
const progress = Math.min(100, (this.frameCount / this.maxFrames) * 100);
|
| 1012 |
+
document.getElementById('progressFill').style.width = progress + '%';
|
| 1013 |
+
},
|
| 1014 |
|
| 1015 |
+
drawNextFrame() {
|
| 1016 |
+
if (this.frameBuffer.length === 0) return;
|
| 1017 |
+
|
| 1018 |
+
const canvas = document.getElementById('outputCanvas');
|
| 1019 |
+
const bitmap = this.frameBuffer.shift();
|
| 1020 |
+
|
| 1021 |
+
canvas.width = bitmap.width;
|
| 1022 |
+
canvas.height = bitmap.height;
|
| 1023 |
+
|
| 1024 |
+
const ctx = canvas.getContext('2d');
|
| 1025 |
+
ctx.drawImage(bitmap, 0, 0);
|
| 1026 |
+
|
| 1027 |
+
if (typeof bitmap.close === 'function') {
|
| 1028 |
+
bitmap.close();
|
| 1029 |
+
}
|
| 1030 |
+
},
|
| 1031 |
+
|
| 1032 |
+
startPlaybackLoop() {
|
| 1033 |
+
if (this.playbackInterval) {
|
| 1034 |
+
clearInterval(this.playbackInterval);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
const fps = parseInt(document.getElementById('playbackFps').value);
|
| 1038 |
+
const intervalMs = Math.max(10, Math.floor(1000 / fps));
|
| 1039 |
+
this.playbackInterval = setInterval(() => this.drawNextFrame(), intervalMs);
|
| 1040 |
},
|
| 1041 |
|
| 1042 |
async handleVideoUpload(event) {
|
|
|
|
| 1078 |
const video = document.getElementById('hiddenVideo');
|
| 1079 |
const canvas = document.getElementById('extractionCanvas');
|
| 1080 |
|
| 1081 |
+
if (!video || !canvas) return null;
|
|
|
|
|
|
|
|
|
|
| 1082 |
|
| 1083 |
const ctx = canvas.getContext('2d');
|
| 1084 |
canvas.width = 832;
|
|
|
|
| 1097 |
return new Promise((resolve) => {
|
| 1098 |
canvas.toBlob(async (blob) => {
|
| 1099 |
if (!blob) {
|
|
|
|
| 1100 |
resolve(null);
|
| 1101 |
return;
|
| 1102 |
}
|
|
|
|
| 1105 |
const uint8Array = new Uint8Array(arrayBuffer);
|
| 1106 |
resolve(uint8Array);
|
| 1107 |
} catch (error) {
|
| 1108 |
+
console.error('Failed to convert blob:', error);
|
| 1109 |
resolve(null);
|
| 1110 |
}
|
| 1111 |
}, 'image/jpeg', 0.9);
|
|
|
|
| 1116 |
const video = document.getElementById('webcamVideo');
|
| 1117 |
const canvas = document.getElementById('extractionCanvas');
|
| 1118 |
|
| 1119 |
+
if (!video || !canvas || !video.videoWidth) return null;
|
|
|
|
|
|
|
|
|
|
| 1120 |
|
| 1121 |
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
| 1122 |
const targetWidth = 832;
|
| 1123 |
const targetHeight = 480;
|
| 1124 |
canvas.width = targetWidth;
|
| 1125 |
canvas.height = targetHeight;
|
| 1126 |
|
|
|
|
| 1127 |
const videoWidth = video.videoWidth;
|
| 1128 |
const videoHeight = video.videoHeight;
|
|
|
|
|
|
|
| 1129 |
const sourceAspect = videoWidth / videoHeight;
|
| 1130 |
const targetAspect = targetWidth / targetHeight;
|
| 1131 |
|
| 1132 |
let sx, sy, sWidth, sHeight;
|
| 1133 |
|
| 1134 |
if (sourceAspect > targetAspect) {
|
|
|
|
| 1135 |
sHeight = videoHeight;
|
| 1136 |
sWidth = videoHeight * targetAspect;
|
| 1137 |
sx = (videoWidth - sWidth) / 2;
|
| 1138 |
sy = 0;
|
| 1139 |
} else {
|
|
|
|
| 1140 |
sWidth = videoWidth;
|
| 1141 |
sHeight = videoWidth / targetAspect;
|
| 1142 |
sx = 0;
|
| 1143 |
sy = (videoHeight - sHeight) / 2;
|
| 1144 |
}
|
| 1145 |
|
|
|
|
| 1146 |
ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, targetWidth, targetHeight);
|
| 1147 |
|
| 1148 |
return new Promise((resolve) => {
|
| 1149 |
canvas.toBlob(async (blob) => {
|
| 1150 |
if (!blob) {
|
|
|
|
| 1151 |
resolve(null);
|
| 1152 |
return;
|
| 1153 |
}
|
|
|
|
| 1156 |
const uint8Array = new Uint8Array(arrayBuffer);
|
| 1157 |
resolve(uint8Array);
|
| 1158 |
} catch (error) {
|
| 1159 |
+
console.error('Failed to convert blob:', error);
|
| 1160 |
resolve(null);
|
| 1161 |
}
|
| 1162 |
}, 'image/jpeg', 0.9);
|
|
|
|
| 1195 |
}
|
| 1196 |
|
| 1197 |
const strengthValue = parseFloat(document.getElementById('strength').value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
const message = {
|
| 1199 |
image: frameBytes,
|
| 1200 |
strength: strengthValue,
|
| 1201 |
timestamp: Date.now()
|
| 1202 |
};
|
| 1203 |
|
| 1204 |
+
// For webcam mode, include prompt and num_blocks
|
| 1205 |
if (this.mode === 'webcam') {
|
| 1206 |
message.prompt = document.getElementById('prompt').value;
|
| 1207 |
message.num_blocks = parseInt(document.getElementById('numBlocks').value);
|
| 1208 |
}
|
| 1209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1210 |
const encoded = createMsgpackEncoder(message);
|
| 1211 |
this.ws.send(encoded);
|
| 1212 |
}, intervalMs);
|
|
|
|
| 1223 |
video.currentTime = 0;
|
| 1224 |
},
|
| 1225 |
|
| 1226 |
+
queuePromptUpdate() {
|
| 1227 |
+
this.pendingPromptUpdate = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
prompt: document.getElementById('prompt').value,
|
| 1229 |
+
num_blocks: parseInt(document.getElementById('numBlocks').value)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
};
|
| 1231 |
|
| 1232 |
+
if (this.promptUpdateTimer) {
|
| 1233 |
+
clearTimeout(this.promptUpdateTimer);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
}
|
| 1235 |
|
| 1236 |
+
this.promptUpdateTimer = setTimeout(() => {
|
| 1237 |
+
if (this.pendingPromptUpdate && this.ws?.readyState === WebSocket.OPEN) {
|
| 1238 |
+
const encoded = createMsgpackEncoder(this.pendingPromptUpdate);
|
| 1239 |
+
this.ws.send(encoded);
|
| 1240 |
+
this.maxFrames = (12 * this.pendingPromptUpdate.num_blocks) - 6;
|
| 1241 |
+
document.getElementById('maxFrames').textContent = this.maxFrames;
|
| 1242 |
+
this.pendingPromptUpdate = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
}
|
| 1244 |
+
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
},
|
| 1246 |
|
| 1247 |
startRecording() {
|
|
|
|
| 1271 |
}
|
| 1272 |
},
|
| 1273 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1274 |
disconnect() {
|
| 1275 |
if (this.ws) {
|
| 1276 |
this.ws.close();
|
|
|
|
| 1280 |
clearInterval(this.playbackInterval);
|
| 1281 |
this.playbackInterval = null;
|
| 1282 |
}
|
| 1283 |
+
if (this.promptUpdateTimer) {
|
| 1284 |
+
clearTimeout(this.promptUpdateTimer);
|
| 1285 |
+
this.promptUpdateTimer = null;
|
| 1286 |
+
}
|
| 1287 |
+
this.pendingPromptUpdate = null;
|
| 1288 |
+
this.stopFrameExtraction();
|
| 1289 |
this.stopWebcam();
|
| 1290 |
+
this.stopRecording();
|
| 1291 |
this.isGenerating = false;
|
| 1292 |
this.updateUI();
|
| 1293 |
},
|
| 1294 |
|
| 1295 |
downloadVideo() {
|
| 1296 |
+
if (this.recordedChunks.length === 0) {
|
| 1297 |
+
this.showError('No video data to download');
|
| 1298 |
+
return;
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
| 1302 |
+
const url = URL.createObjectURL(blob);
|
| 1303 |
+
const a = document.createElement('a');
|
| 1304 |
+
a.href = url;
|
| 1305 |
+
a.download = `generated-video-${Date.now()}.webm`;
|
| 1306 |
+
a.click();
|
| 1307 |
+
URL.revokeObjectURL(url);
|
| 1308 |
+
this.showInfo('Video downloaded successfully');
|
| 1309 |
},
|
| 1310 |
|
| 1311 |
updateUI() {
|