// ==UserScript==
// @name Sora Moderation Helper
// @namespace https://lugia19.com
// @version 0.12.1
// @description Monitor image generation status from Sora, Auto-Retry, and filter only for successes
// @author lugia19
// @license MIT
// @match https://sora.com/*
// @grant GM_setValue
// @grant GM_getValue
// @inject-into page
// ==/UserScript==
(function () {
'use strict';
let lastUrl = null;
let currentUIRetryCount = 1;
let mainUI = null;
let retryTracker = null;
let retrySubmitter = null;
let pollingInterval = null;
let authToken = null;
let lastInfo = null; // Track the last known status
const MAX_AUTO_RETRIES = 30;
/**
* Base class for managing the status card UI
*/
class MainUI {
constructor() {
this.card = null;
this.contentDiv = null;
this.init();
}
init() {
if (this.card) return this.card;
this.card = document.createElement('div');
this.card.id = 'generation-status-card';
this.card.style.cssText = `
position: fixed;
top: 80vh;
right: 20px;
width: 300px;
padding: 15px;
background-color: black;
color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
border: 2px solid #aaa;
border-radius: 6px;
z-index: 99999;
font-family: Arial, sans-serif;
display: none;
`;
const header = document.createElement('div');
header.style.cssText = `
margin-bottom: 10px;
font-weight: bold;
padding-bottom: 5px;
border-bottom: 1px solid #444;
`;
header.textContent = 'Generation Status';
this.contentDiv = document.createElement('div');
this.contentDiv.id = 'generation-status-content';
this.card.appendChild(header);
this.card.appendChild(this.contentDiv);
// Find container and append card
let container = this.findContainer();
container.appendChild(this.card);
return this.card;
}
findContainer() {
// Find a suitable container
let container = document.querySelector('div[role="dialog"][data-state="open"]');
if (!container) {
container = document.querySelector('#__next') ||
document.querySelector('.root') ||
document.body;
}
return container;
}
updateStatus(taskInfo) {
const { displayStatus, borderColor } = this.determineStatusAndColor(taskInfo);
this.card.style.borderColor = borderColor;
// Clear existing task status elements
this.clearUI();
// Add task info elements
this.addTaskInfoElements(taskInfo, displayStatus);
// Show the card
this.show();
return { displayStatus, borderColor };
}
clearUI() {
// Keep UI components with specific IDs
const children = Array.from(this.contentDiv.children);
for (const child of children) {
this.contentDiv.removeChild(child);
}
}
addTaskInfoElements(taskInfo, displayStatus) {
const taskIdDiv = document.createElement('div');
taskIdDiv.innerHTML = `<strong>Task ID:</strong> ${taskInfo.id.replace('task_', '')}`;
this.contentDiv.insertBefore(taskIdDiv, this.contentDiv.firstChild);
const statusDiv = document.createElement('div');
statusDiv.innerHTML = `<strong>Status:</strong> ${displayStatus}`;
this.contentDiv.insertBefore(statusDiv, this.contentDiv.childNodes[1] || null);
const progressDiv = document.createElement('div');
progressDiv.innerHTML = `<strong>Progress:</strong> ${taskInfo.progress_pct !== undefined ? Math.round(taskInfo.progress_pct * 100) : 'undefined'}%`;
this.contentDiv.insertBefore(progressDiv, this.contentDiv.childNodes[2] || null);
}
determineStatusAndColor(taskInfo) {
let displayStatus = "N/A";
let borderColor = "#aaa";
if (taskInfo.status === "succeeded" && taskInfo.num_unsafe_generations < taskInfo.n_variants) {
displayStatus = "Succeeded";
borderColor = "#4CAF50"; // Green for success
} else if (taskInfo.status === "running") {
displayStatus = "Running";
borderColor = "#aaa"; // Grey for running
} else {
// Check rejection conditions
if (taskInfo.failure_reason === "input_moderation") {
displayStatus = "Rejected (Input Filter)";
borderColor = "#F44336"; // Red for rejection
} else if (taskInfo.failure_reason === "output_moderation") {
displayStatus = "Rejected (Output Filter)";
borderColor = "#F44336"; // Red for rejection
} else if (
taskInfo.num_unsafe_generations !== undefined &&
taskInfo.n_variants !== undefined &&
taskInfo.num_unsafe_generations >= taskInfo.n_variants
) {
displayStatus = "Rejected (Unsafe Filter)";
borderColor = "#F44336"; // Red for rejection
} else if (taskInfo.status === "failed") {
displayStatus = "Failed";
borderColor = "#F44336"; // Red for failure
}
}
return { displayStatus, borderColor };
}
show() {
this.card.style.display = 'block';
}
hide() {
this.card.style.display = 'none';
}
isTaskFailed(taskInfo) {
const { displayStatus } = this.determineStatusAndColor(taskInfo);
return taskInfo.status === 'failed' || displayStatus.includes('Rejected');
}
}
/**
* Class for displaying retry chain progress
*/
class RetryTracker {
constructor(mainUI) {
this.mainUI = mainUI;
this.container = null;
}
show(chain) {
this.remove(); // Remove any existing tracker
// Create container
this.container = document.createElement('div');
this.container.id = 'retry-tracker-container';
this.container.style.cssText = `
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #444;
display: flex;
justify-content: space-between;
align-items: center;
`;
// Progress message
const progressMessage = document.createElement('div');
progressMessage.style.color = '#FFC107';
progressMessage.innerHTML = `<strong>Auto-Retry in progress:</strong> ${chain.current}/${chain.total} attempts remaining`;
// Stop button
const stopButton = document.createElement('button');
stopButton.textContent = 'Stop';
stopButton.style.cssText = `
background: #F44336;
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
`;
stopButton.addEventListener('mouseover', () => {
stopButton.style.background = '#D32F2F';
});
stopButton.addEventListener('mouseout', () => {
stopButton.style.background = '#F44336';
});
// Add click handler
stopButton.addEventListener('click', () => {
const currentTaskId = getCurrentTaskId();
RetryChain.removeChainByTaskId(currentTaskId);
console.log('Auto-retry stopped by user');
this.remove();
// Check if we should show the submitter
if (lastInfo && this.mainUI.isTaskFailed(lastInfo)) {
const submitter = new RetrySubmitter(this.mainUI);
submitter.show();
}
});
// Assemble container
this.container.appendChild(progressMessage);
this.container.appendChild(stopButton);
// Add to main UI
this.mainUI.contentDiv.appendChild(this.container);
}
remove() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
this.container = null;
}
}
}
/**
* Class for the UI to submit a new retry
*/
class RetrySubmitter {
constructor(mainUI) {
this.mainUI = mainUI;
this.container = null;
}
show() {
this.remove(); // Remove any existing submitter
// Create container
this.container = document.createElement('div');
this.container.id = 'retry-submitter-container';
this.container.style.cssText = `
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #444;
display: flex;
align-items: center;
gap: 8px;
`;
// Label
const retryLabel = document.createElement('label');
retryLabel.textContent = 'Auto-Retry:';
// Input
const retryInput = document.createElement('input');
retryInput.id = 'auto-retry-input';
retryInput.type = 'number';
retryInput.min = '1';
retryInput.max = MAX_AUTO_RETRIES.toString();
retryInput.value = currentUIRetryCount.toString();
retryInput.style.cssText = `
width: 50px;
background: #333;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 4px;
`;
retryInput.addEventListener('input', () => {
let value = parseInt(retryInput.value);
if (!isNaN(value)) {
currentUIRetryCount = value;
}
});
// Clamp input value
retryInput.addEventListener('blur', () => {
let value = parseInt(retryInput.value);
if (isNaN(value) || value < 1) {
value = 1;
} else if (value > MAX_AUTO_RETRIES) {
value = MAX_AUTO_RETRIES;
}
retryInput.value = value.toString();
currentUIRetryCount = value;
});
// Button
const retryButton = document.createElement('button');
retryButton.textContent = 'Begin';
retryButton.style.cssText = `
background: #444;
color: white;
border: 1px solid #666;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
`;
retryButton.addEventListener('mouseover', () => {
retryButton.style.background = '#555';
});
retryButton.addEventListener('mouseout', () => {
retryButton.style.background = '#444';
});
// Click handler for the Begin button
retryButton.addEventListener('click', async () => {
// Use the current UI value stored in the global variable
const count = currentUIRetryCount;
if (count > 0 && count <= MAX_AUTO_RETRIES) {
const currentTaskId = getCurrentTaskId();
let newChain = null;
try {
// Fetch the current task's settings
console.log(`Fetching settings for task ${currentTaskId}`);
const response = await fetch(`https://sora.com/backend/video_gen/${currentTaskId}`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const taskInfo = await response.json();
// Create the retry chain
newChain = RetryChain.createChain(currentTaskId, count);
// Extract relevant settings to preserve
const settings = {
n_variants: taskInfo.n_variants || 2,
prompt: taskInfo.prompt,
height: taskInfo.height,
width: taskInfo.width,
operation: taskInfo.operation,
inpaint_items: taskInfo.inpaint_items,
remix_config: taskInfo.remix_config,
model: taskInfo.model
};
// Store settings in the chain
newChain.settings = { ...settings };
RetryChain.updateChain(newChain);
console.log('Stored original task settings for retry:', settings);
} else {
console.error('Failed to fetch task settings');
// Fall back to basic chain
newChain = RetryChain.createChain(currentTaskId, count);
}
} catch (error) {
console.error('Error fetching task settings:', error);
// Fall back to basic chain
newChain = RetryChain.createChain(currentTaskId, count);
} finally {
if (newChain) {
// Common UI update code
this.remove();
const tracker = new RetryTracker(this.mainUI);
tracker.show(newChain);
// Begin retry
performAutoRetry(true);
}
}
}
});
// Assemble container
this.container.appendChild(retryLabel);
this.container.appendChild(retryInput);
this.container.appendChild(retryButton);
// Add to main UI
this.mainUI.contentDiv.appendChild(this.container);
}
remove() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
this.container = null;
}
}
}
/**
* Class representing an auto-retry chain for Sora generations
*/
class RetryChain {
/**
* Create a new retry chain
* @param {string} initialTaskId - The first task ID in the chain
* @param {number} totalRetries - Total number of retries for this chain
*/
constructor(initialTaskId, totalRetries) {
this.taskIds = [initialTaskId];
this.generationIds = [];
this.total = totalRetries;
this.current = totalRetries;
this.settings = null;
}
/**
* Add a new task ID to this chain
* @param {string} taskId - The task ID to add
*/
addTask(taskId) {
if (!this.taskIds.includes(taskId)) {
this.taskIds.push(taskId);
}
}
/**
* Add a generation ID to this chain
* @param {string} genId - The generation ID to add
*/
addGenerationId(genId) {
if (!this.generationIds.includes(genId)) {
this.generationIds.push(genId);
}
}
/**
* Check if a task ID is part of this chain
* @param {string} taskId - Task ID to check
* @returns {boolean} True if task is in this chain
*/
containsTask(taskId) {
return this.taskIds.includes(taskId);
}
/**
* Check if a generation ID is part of this chain
* @param {string} genId - Generation ID to check
* @returns {boolean} True if generation is in this chain
*/
containsGeneration(genId) {
return this.generationIds.includes(genId);
}
/**
* Check if this chain has retries remaining
* @returns {boolean} True if retries remain
*/
hasRetriesLeft() {
return this.current > 0;
}
/**
* Get most recent task ID in this chain
* @returns {string} Most recent task ID
*/
getLatestTaskId() {
return this.taskIds[this.taskIds.length - 1];
}
// Static methods to manage chains in storage
/**
* Save all chains to storage, filtering out expired chains
* @param {RetryChain[]} chains - Array of RetryChain objects
*/
static saveChains(chains) {
// Convert the class instances to plain objects for storage
const plainChains = chains.map(chain => ({
taskIds: chain.taskIds,
generationIds: chain.generationIds,
total: chain.total,
current: chain.current,
settings: chain.settings
}));
GM_setValue('autoRetryChains', plainChains);
}
/**
* Load all chains from storage, filtering out expired chains
* @returns {RetryChain[]} Array of RetryChain objects
*/
static loadChains() {
const plainChains = GM_getValue('autoRetryChains', []);
// Convert the plain objects back to class instances, INCLUDING zero length ones.
return plainChains.map(chain => {
const newChain = new RetryChain('', 0);
newChain.taskIds = chain.taskIds;
newChain.generationIds = chain.generationIds || [];
newChain.total = chain.total;
newChain.current = chain.current;
newChain.settings = chain.settings;
return newChain;
});
}
/**
* Clean up all expired chains (chains with no retries left)
* @returns {number} Number of chains removed
*/
static cleanupExpiredChains() {
const chains = RetryChain.loadChains();
const initialCount = chains.length;
// Filter to keep only chains with retries left
const activeChains = chains.filter(chain => chain.current > 0);
if (activeChains.length !== initialCount) {
RetryChain.saveChains(activeChains);
console.log(`Cleaned up ${initialCount - activeChains.length} expired retry chains`);
return initialCount - activeChains.length;
}
return 0;
}
/**
* Find a chain containing a specific task ID
* @param {string} taskId - Task ID to search for
* @returns {RetryChain|null} Matching chain or null if not found
*/
static findChainByTaskId(taskId) {
const chains = RetryChain.loadChains();
return chains.find(chain => chain.containsTask(taskId)) || null;
}
/**
* Find a chain containing a specific generation ID
* @param {string} genId - Generation ID to search for
* @returns {RetryChain|null} Matching chain or null if not found
*/
static findChainByGenerationId(genId) {
const chains = RetryChain.loadChains();
return chains.find(chain => chain.containsGeneration(genId)) || null;
}
/**
* Create a new chain and save it
* @param {string} taskId - Initial task ID for the chain
* @param {number} totalRetries - Total retries for this chain
* @returns {RetryChain} The newly created chain
*/
static createChain(taskId, totalRetries) {
const chains = RetryChain.loadChains();
const newChain = new RetryChain(taskId, totalRetries);
chains.push(newChain);
RetryChain.saveChains(chains);
return newChain;
}
/**
* Remove a chain containing a specific task ID
* @param {string} taskId - Task ID in the chain to remove
* @returns {boolean} True if a chain was removed
*/
static removeChainByTaskId(taskId) {
const chains = RetryChain.loadChains();
const initialLength = chains.length;
const filteredChains = chains.filter(chain => !chain.containsTask(taskId));
if (filteredChains.length !== initialLength) {
RetryChain.saveChains(filteredChains);
return true;
}
return false;
}
/**
* Update an existing chain in storage
* @param {RetryChain} updatedChain - The chain with updated values
*/
static updateChain(updatedChain) {
const chains = RetryChain.loadChains();
const index = chains.findIndex(chain =>
chain.taskIds.some(id => updatedChain.taskIds.includes(id))
);
if (index !== -1) {
chains[index] = updatedChain;
RetryChain.saveChains(chains);
}
}
/**
* Check if a task ID is the latest in this chain
* @param {string} taskId - Task ID to check
* @returns {boolean} True if task is the latest in the chain
*/
isLatestTask(taskId) {
if (this.taskIds.length === 0) return false;
return this.taskIds[this.taskIds.length - 1] === taskId;
}
/**
* Static method to check if a task is the latest in its chain
* @param {string} taskId - Task ID to check
* @returns {boolean} True if task is the latest in its chain
*/
static isLatestTaskInChain(taskId) {
const chain = RetryChain.findChainByTaskId(taskId);
if (!chain) return false;
return chain.isLatestTask(taskId);
}
}
RetryChain.cleanupExpiredChains();
function getSuccessesOnlyFilter() {
return GM_getValue('successesOnlyFilter', false);
}
function setSuccessesOnlyFilter(value) {
GM_setValue('successesOnlyFilter', value);
}
// Function to perform auto-retry
async function performAutoRetry(immediate = false) {
if (!immediate) await new Promise(r => setTimeout(r, 3000)); // Wait for 3 seconds before retrying
const currentTaskId = getCurrentTaskId();
const chain = RetryChain.findChainByTaskId(currentTaskId);
if (!chain) return; // No chain found for this task
console.log(`Auto-retry initiated, attempts remaining: ${chain.current}`);
// Decrement the retry counter IF we're switching chains.
if (unsafeWindow.currentRetryTaskId != currentTaskId) {
chain.current--;
RetryChain.updateChain(chain);
}
if (chain.current < 0) {
RetryChain.removeChainByTaskId(currentTaskId);
return;
}
// Set a temporary flag just for the request interception to work
unsafeWindow.currentRetryTaskId = currentTaskId;
try {
// Step 1: Find and click the Remix button on the main page
await new Promise(r => setTimeout(r, 3000));
const remixContainer = document.querySelector('.flex.w-full.items-center.justify-center.tablet\\:w-1\\/2');
if (remixContainer) {
const button = remixContainer.querySelector('button');
if (button) {
console.log('Found remix button, clicking...');
button.click();
// Wait for the dialog to appear
await new Promise(resolve => setTimeout(resolve, 3000));
// Step 2: Handle the dialog that appears
// Case 1: Dialog with a "Remix" button
const remixButton = Array.from(document.querySelectorAll('button')).find(btn =>
btn.textContent.trim() === 'Remix' &&
btn.closest('[role="dialog"]')
);
if (remixButton) {
console.log('Found Remix button in dialog, clicking...');
remixButton.click();
// Set a flag to indicate we're in auto-retry mode
unsafeWindow.popupRetryInProgress = true;
return;
}
// Case 2: Dialog with a "Create image" button (with up arrow SVG)
const createButtons = document.querySelectorAll('[data-state="closed"] svg');
for (const svg of createButtons) {
// Look for the SVG path with the up arrow path data
const path = svg.querySelector('path[d*="11.293 5.293"]');
if (path) {
// Found the create image button
const createButton = svg.closest('button');
if (createButton) {
console.log('Found Create image button in dialog, clicking...');
createButton.click();
unsafeWindow.popupRetryInProgress = true;
return;
}
}
}
// Case 3: Left for future implementation
console.log('No recognized dialog buttons found, may need to implement additional case');
} else {
console.error('Could not find button in remix container');
}
} else {
console.error('Could not find remix container');
}
} catch (error) {
console.error('Error during auto-retry:', error);
}
// If we get here, something went wrong with the retry attempt
// We'll reload the page anyway as a fallback
console.log('Auto-retry failed, reloading page...');
setTimeout(() => {
window.location.reload();
}, 1000);
}
// Function to show browser notification
function showNotification(title, message) {
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
body: message,
icon: 'https://sora.com/favicon.ico' // Sora favicon as icon
});
notification.onclick = function () {
unsafeWindow.focus();
notification.close();
};
// Auto close after 10 seconds
setTimeout(() => notification.close(), 10000);
}
}
// Create the status card once
function initUI() {
if (!mainUI) {
mainUI = new MainUI();
retryTracker = new RetryTracker(mainUI);
retrySubmitter = new RetrySubmitter(mainUI);
}
return mainUI;
}
function getCurrentTaskId() {
const match = location.pathname.match(/\/t\/(task_[a-zA-Z0-9]+)/);
return match ? match[1] : null;
}
// Function to update the status card with taskInfo
function updateStatusCard(taskInfo) {
const ui = initUI();
const { displayStatus, borderColor } = ui.updateStatus(taskInfo);
const currentTaskId = getCurrentTaskId();
const chain = RetryChain.findChainByTaskId(currentTaskId);
const isFailed = ui.isTaskFailed(taskInfo);
// Detect state transitions from running to completed for notifications
if (lastInfo && lastInfo.status === 'running' && taskInfo.status !== 'running') {
if (taskInfo.status === 'succeeded' && !isFailed) {
// Always notify on success
showNotification(
'Sora Generation Complete',
`Your generation is successful! ${taskInfo.id}`
);
// If this was part of a chain, remove it
if (chain) {
RetryChain.removeChainByTaskId(currentTaskId);
retryTracker.remove();
}
// Reload page after success
setTimeout(() => {
window.location.reload();
}, 1000);
} else if (isFailed && !chain) {
// Only notify on failure if not part of a retry chain
showNotification(
'Sora Generation Failed',
`Your generation was ${displayStatus.toLowerCase()}! ${taskInfo.id}`
);
// Reload page after failed state with no chain
setTimeout(() => {
window.location.reload();
}, 1000);
} else if (taskInfo.status == "cancelled") {
if (chain) {
RetryChain.removeChainByTaskId(currentTaskId);
retryTracker.remove();
}
}
}
// Handle retry UI and retry logic independently of state transitions
if (chain) {
// Show retry tracker for any task in a chain
retryTracker.show(chain);
// If task has failed and has retries left, initiate a retry
// This happens regardless of previous state
if (isFailed && chain.hasRetriesLeft() && !unsafeWindow.currentRetryTaskId) {
console.log(`Task failed, auto-retrying (${chain.current} left)`);
performAutoRetry();
}
} else if (isFailed) {
// No chain but task failed, show retry submitter
retrySubmitter.show();
} else {
// Not failed and not in chain, remove both UI elements
retryTracker.remove();
retrySubmitter.remove();
}
// Store last info
lastInfo = { ...taskInfo };
ui.show();
}
// Fetch task status directly
async function fetchTaskStatus(taskId) {
if (!authToken) {
console.error('No auth token available for API request');
return null;
}
try {
const response = await fetch(`https://sora.com/backend/video_gen/${taskId}`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const taskInfo = await response.json();
updateStatusCard(taskInfo);
return taskInfo;
} catch (error) {
console.error('Error fetching task status:', error);
return null;
}
}
// Start polling for task status
function startTaskPolling(taskId) {
// Clear any existing polling
stopTaskPolling();
// Reset last status when starting new polling
lastInfo = null;
// Fetch immediately, then start polling
fetchTaskStatus(taskId);
pollingInterval = setInterval(() => {
fetchTaskStatus(taskId).then(taskInfo => {
// If task is complete or failed, we can stop polling
if (taskInfo && (taskInfo.status === 'succeeded' || taskInfo.status === 'failed')) {
// Continue polling for a short time after completion to ensure we get final status
setTimeout(() => {
stopTaskPolling();
}, 30000); // Keep polling for 30 seconds after completion
}
});
}, 10000); // Poll every 10 seconds
}
// Stop polling for task status
function stopTaskPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
}
// Filtering changes
function addSuccessesOnlyFilterOption() {
// Find the filter menu
const filterMenu = document.querySelector('[role="menu"][data-radix-menu-content]');
if (!filterMenu) return false;
// Check if our option is already added
if (filterMenu.querySelector('#successes-only-filter')) return true;
// Create our checkbox item based on the existing ones
const successesOnlyItem = document.createElement('div');
successesOnlyItem.id = 'successes-only-filter';
successesOnlyItem.setAttribute('role', 'menuitemcheckbox');
successesOnlyItem.setAttribute('aria-checked', getSuccessesOnlyFilter() ? 'true' : 'false');
successesOnlyItem.setAttribute('data-state', getSuccessesOnlyFilter() ? 'checked' : 'unchecked');
successesOnlyItem.setAttribute('tabindex', '-1');
successesOnlyItem.setAttribute('data-orientation', 'vertical');
successesOnlyItem.setAttribute('data-radix-collection-item', '');
successesOnlyItem.className = 'group relative flex cursor-default select-none items-center gap-2 rounded-[10px] px-2 py-2.5 text-sm outline-none focus:bg-token-bg-light data-[disabled]:pointer-events-none data-[disabled]:opacity-50';
// Content
successesOnlyItem.innerHTML = `
<div class="flex flex-1 items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-[18px] w-[18px]">
<path fill="currentColor" d="M21.043 3.393a2.16 2.16 0 0 1 .523 3.007l-10.25 14.564a2.158 2.158 0 0 1-3.36.21L2.56 15.24a2.158 2.158 0 0 1 3.193-2.903l3.583 3.941 8.7-12.362a2.16 2.16 0 0 1 3.006-.523"></path>
</svg>Successes Only (SLOW!)
</div>
<div class="rounded-md border border-token-bg-active group-data-[state=checked]:bg-token-bg-inverse group-data-[state=checked]:border-token-bg-inverse flex h-4 w-4 items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-2.5 w-2.5 text-token-bg-primary opacity-0 group-data-[state=checked]:opacity-100">
<path fill="currentColor" d="M21.043 3.393a2.16 2.16 0 0 1 .523 3.007l-10.25 14.564a2.158 2.158 0 0 1-3.36.21L2.56 15.24a2.158 2.158 0 0 1 3.193-2.903l3.583 3.941 8.7-12.362a2.16 2.16 0 0 1 3.006-.523"></path>
</svg>
</div>
`;
// Add click handler
successesOnlyItem.addEventListener('click', function () {
const isChecked = this.getAttribute('data-state') === 'checked';
const newState = !isChecked;
this.setAttribute('data-state', newState ? 'checked' : 'unchecked');
this.setAttribute('aria-checked', newState ? 'true' : 'false');
setSuccessesOnlyFilter(newState);
// Log the change
console.log('Successes Only filter set to:', newState);
// Reload the page to apply the filter
setTimeout(() => {
window.location.reload();
}, 100); // Small delay to ensure the setting is saved
});
// Add our item to the menu - after all the existing items
filterMenu.appendChild(successesOnlyItem);
return true;
}
// Function to monitor for the filter menu appearing
function monitorForFilterMenu() {
let attempts = 0;
const maxAttempts = 20; // 20 * 50ms = 1 second max wait time
let checkInterval;
function checkForFilterMenu() {
attempts++;
if (addSuccessesOnlyFilterOption()) {
// Success! Menu found and option added
clearInterval(checkInterval);
} else if (attempts >= maxAttempts) {
// Give up after max attempts
clearInterval(checkInterval);
console.log('Filter menu not found after maximum attempts');
}
}
checkInterval = setInterval(checkForFilterMenu, 50);
}
// Monkeypatch fetch for auto retry and filter
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function (input, init) {
// Extract the auth token from outgoing requests
if (init && init.headers) {
const authHeader = init.headers.Authorization || init.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
authToken = authHeader.substring(7); // Remove 'Bearer ' prefix
}
}
// Handle the GET requests to filter library results
if (typeof input === 'string' &&
input.includes('backend/video_gen') &&
input.includes('limit=') &&
init && init.method === 'GET' && location.href.includes("library")) {
// Check if Successes Only filter is enabled
const successesOnlyEnabled = getSuccessesOnlyFilter();
if (successesOnlyEnabled) {
const url = new URL("https://sora.com" + input);
const limitParam = url.searchParams.get('limit');
const beforeParam = url.searchParams.get('before');
const afterParam = url.searchParams.get('after');
const isPaginatingBackwards = Boolean(beforeParam);
console.log(`Intercepting library request with Successes Only filter enabled. Direction: ${isPaginatingBackwards ? 'backwards' : 'forwards'}`);
// Make the original request
const response = await originalFetch(input, init);
const originalResponseData = await response.clone().json();
// If there are no task_responses or it's not an array, just return the original response
if (!originalResponseData.task_responses || !Array.isArray(originalResponseData.task_responses)) {
return response;
}
// Begin collecting successful tasks
let allSuccessfulTasks = [];
let currentPaginationId = null;
let hasMore = originalResponseData.has_more;
// Add successful tasks from the current response
const successfulTasks = originalResponseData.task_responses.filter(task =>
task.status === 'succeeded' &&
(task.num_unsafe_generations === undefined ||
task.n_variants === undefined ||
task.num_unsafe_generations < task.n_variants)
);
allSuccessfulTasks = allSuccessfulTasks.concat(successfulTasks);
// Set the appropriate ID for pagination based on direction
if (isPaginatingBackwards) {
currentPaginationId = originalResponseData.first_id;
} else {
currentPaginationId = originalResponseData.last_id;
}
// If we don't have enough successful tasks (50) and there are more available, fetch more
while (allSuccessfulTasks.length < 50 && hasMore && currentPaginationId) {
console.log(`Need more tasks, currently have ${allSuccessfulTasks.length}, fetching more ${isPaginatingBackwards ? 'backwards' : 'forwards'}...`);
// Construct the URL for the next page based on pagination direction
const nextPageUrl = new URL(url.origin + url.pathname);
nextPageUrl.searchParams.set('limit', limitParam || '50');
if (isPaginatingBackwards) {
nextPageUrl.searchParams.set('before', currentPaginationId);
} else {
nextPageUrl.searchParams.set('after', currentPaginationId);
}
try {
const nextPageResponse = await originalFetch(nextPageUrl.toString(), init);
const nextPageData = await nextPageResponse.json();
if (!nextPageData.task_responses || !Array.isArray(nextPageData.task_responses)) {
break; // Something went wrong, stop fetching
}
// Filter for successful tasks
const nextSuccessfulTasks = nextPageData.task_responses.filter(task =>
task.status === 'succeeded' &&
(task.num_unsafe_generations === undefined ||
task.n_variants === undefined ||
task.num_unsafe_generations < task.n_variants)
);
// Add to our collection
allSuccessfulTasks = allSuccessfulTasks.concat(nextSuccessfulTasks);
// Update for next iteration
hasMore = nextPageData.has_more;
if (isPaginatingBackwards) {
currentPaginationId = nextPageData.first_id;
} else {
currentPaginationId = nextPageData.last_id;
}
if (!hasMore || !currentPaginationId) {
break; // No more pages to fetch
}
} catch (error) {
console.error('Error fetching additional tasks:', error);
break;
}
}
// Limit to 50 tasks maximum
allSuccessfulTasks = allSuccessfulTasks.slice(0, 50);
console.log(`Returning ${allSuccessfulTasks.length} successful tasks for ${isPaginatingBackwards ? 'backwards' : 'forwards'} pagination`);
// Create modified response with just the successful tasks
const modifiedResponse = {
...originalResponseData,
task_responses: allSuccessfulTasks,
// If we have exactly 50 successful tasks and hasMore was true, there might be more
has_more: allSuccessfulTasks.length >= 50 && hasMore
};
// Set the appropriate first_id or last_id based on pagination direction
if (isPaginatingBackwards && allSuccessfulTasks.length > 0) {
modifiedResponse.first_id = allSuccessfulTasks[0].id;
}
if (!isPaginatingBackwards && allSuccessfulTasks.length > 0) {
modifiedResponse.last_id = allSuccessfulTasks[allSuccessfulTasks.length - 1].id;
}
// Create a new Response object with our modified data
return new Response(JSON.stringify(modifiedResponse), {
status: 200,
statusText: 'OK',
headers: response.headers
});
}
}
const chain = (unsafeWindow.currentRetryTaskId) ? RetryChain.findChainByTaskId(unsafeWindow.currentRetryTaskId) : null;
if (chain) {
console.log("Found chain:")
console.log(chain)
}
if (typeof input === 'string' &&
input.includes('/g/gen_') &&
unsafeWindow.currentRetryTaskId) {
try {
// Extract the generation ID from the URL
const genIdMatch = input.match(/\/g\/(gen_[a-zA-Z0-9]+)/);
if (genIdMatch && genIdMatch[1]) {
const genId = genIdMatch[1];
console.log(`Detected navigation to generation ${genId} during retry of task ${unsafeWindow.currentRetryTaskId}`);
if (chain) {
// Add the generation ID to the chain
chain.addGenerationId(genId);
RetryChain.updateChain(chain);
console.log(`Added generation ID ${genId} to retry chain`);
// Store this for potential URL change handler
unsafeWindow.lastRetryGenId = genId;
}
}
} catch (error) {
console.error('Error tracking generation ID:', error);
}
}
// In the fetch monkeypatch
if (typeof input === 'string' &&
input.includes('backend/video_gen') &&
init && init.method === 'POST' &&
unsafeWindow.currentRetryTaskId) {
try {
if (chain) {
// Get the stored settings
const settings = chain.settings;
console.log("Got settings", settings)
if (settings) {
// Parse the original body
const body = JSON.parse(init.body);
console.log('Original request body:', JSON.stringify(body));
// Apply all the stored settings from the original task by iterating over properties
for (const [key, value] of Object.entries(settings)) {
if (value !== undefined) {
body[key] = value;
}
}
// Update the request body
init.body = JSON.stringify(body);
console.log('Applied original settings to new generation request', JSON.stringify(body));
}
} else {
console.log('No chain found for task ID:', unsafeWindow.currentRetryTaskId);
}
} catch (error) {
console.error('Error applying stored settings to request:', error);
}
}
const response = await originalFetch(input, init);
const responseClone = response.clone();
console.log("Current retry task ID:", unsafeWindow.currentRetryTaskId)
// When capturing the response after a retry
if (typeof input === 'string' &&
input.includes('backend/video_gen') &&
init && init.method === 'POST' &&
unsafeWindow.currentRetryTaskId) {
try {
// Get the body from the response
const body = await responseClone.json();
if (body && body.id) {
const newTaskId = body.id;
if (chain) {
// Add the new task to the chain
chain.addTask(newTaskId);
RetryChain.updateChain(chain);
}
// Clear the temporary flag
unsafeWindow.currentRetryTaskId = null;
// Navigate to the new task
console.log(`Auto-retry successful, navigating to task: ${newTaskId}`);
unsafeWindow.location.href = `https://sora.com/t/${newTaskId}`;
}
} catch (error) {
console.error('Error capturing new task ID:', error);
}
}
if (typeof input === 'string') {
try {
// Check for auth token in response headers (alternative method)
const authHeader = response.headers.get('Authorization') || response.headers.get('authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
authToken = authHeader.substring(7);
console.log('Auth token captured from response');
}
const currentTaskId = getCurrentTaskId();
if (currentTaskId) {
let taskInfo = null;
if (input.includes('backend/notif')) {
const response = await responseClone.json();
if (response && Array.isArray(response.data)) {
taskInfo = response.data.find(item =>
item.payload && item.payload.id === currentTaskId
)?.payload;
}
} else if (input.match(/\/backend\/video_gen\/task_[a-zA-Z0-9]+/)) {
const data = await responseClone.json();
if (data.id === currentTaskId) {
taskInfo = data;
}
}
if (taskInfo) {
updateStatusCard(taskInfo);
}
}
} catch (error) {
console.error('Error processing response:', error);
}
}
return response;
};
// Check URL changes for task retry and filter addon
setInterval(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
// Reset UI if URL changes
if (mainUI) {
// Remove existing UI elements
const cardElement = document.getElementById('generation-status-card');
if (cardElement && cardElement.parentNode) {
cardElement.parentNode.removeChild(cardElement);
}
// Reset UI instances
mainUI = null;
retryTracker = null;
retrySubmitter = null;
}
// Check if we're on a generation page with remix parameter
if (currentUrl.includes('/g/gen_')) {
// Extract the generation ID
const genIdMatch = currentUrl.match(/\/g\/(gen_[a-zA-Z0-9]+)/);
if (genIdMatch && genIdMatch[1]) {
const genId = genIdMatch[1];
// First, check if this generation is part of any retry chain
let chain = RetryChain.findChainByGenerationId(genId);
// If not found but we have a last retry gen ID, use that
if (!chain && unsafeWindow.lastRetryGenId === genId) {
// This is a new generation page we just navigated to in a retry
const sourceTaskId = unsafeWindow.currentRetryTaskId;
if (sourceTaskId) {
chain = RetryChain.findChainByTaskId(sourceTaskId);
if (chain) {
// Add this generation ID to the chain
chain.addGenerationId(genId);
RetryChain.updateChain(chain);
console.log(`Added generation ID ${genId} to retry chain from URL change handler`);
}
}
}
// If this generation is part of a retry chain and has the remix parameter
if (chain && currentUrl.includes('?remix=') && chain.hasRetriesLeft()) {
console.log('Detected gen page with remix parameter, auto-retry in progress');
// Look for the Remix button on this page and click it
setTimeout(() => {
const remixButtons = Array.from(document.querySelectorAll('button')).filter(btn =>
btn.textContent.trim() === 'Remix'
);
if (remixButtons.length > 0) {
console.log('Found Remix button on gen page, clicking...');
remixButtons[0].click();
// Set the latest task ID for tracking in request interception
unsafeWindow.currentRetryTaskId = chain.getLatestTaskId();
} else {
console.error('Could not find Remix button on gen page');
// Reload page
window.location.reload();
}
}, 1000); // Wait a second for the page to fully load
}
}
} else {
}
// Handle library filter setup
if (currentUrl.includes('sora.com/library')) {
console.log('On library page, setting up filter button monitoring');
// Find the filter button (using the existing setupFilterButtonMonitoring function)
const setupFilterButtonMonitoring = () => {
// Look for button with filter SVG path
const filterButtons = document.querySelectorAll('button');
let filterButton = null;
for (const button of filterButtons) {
// Check for the "Filter" text in screen reader span
const srSpan = button.querySelector('.sr-only');
if (srSpan && srSpan.textContent === 'Filter') {
filterButton = button.closest('[aria-haspopup="menu"]');
break;
}
}
if (filterButton) {
console.log("Adding listener...")
// Add click listener to the filter button
filterButton.addEventListener('pointerdown', function () {
console.log('Filter button pointerdown, monitoring for menu');
monitorForFilterMenu();
});
} else {
// Button not found yet, try again after a short delay
setTimeout(setupFilterButtonMonitoring, 500);
}
};
setupFilterButtonMonitoring();
}
// Get current task ID
const currentTaskId = getCurrentTaskId();
if (currentTaskId) {
// Start polling for this task
startTaskPolling(currentTaskId);
RetryChain.cleanupExpiredChains(); //Clean up expired chains
// Initialize UI
initUI();
// Check if this task is part of a retry chain
const chain = RetryChain.findChainByTaskId(currentTaskId);
if (chain) {
// Show retry tracker for this chain
retryTracker.show(chain);
}
} else {
// Not on a task page, stop polling and hide the card
stopTaskPolling();
if (mainUI) {
mainUI.hide();
}
}
}
}, 1000);
console.log('Sora Generation Status Monitor initialized');
})();