Spaces:
Running
Running
<html> | |
<head> | |
<title>Watermark Detector</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> | |
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="d-flex justify-content-between align-items-center"> | |
<h1>Interactive watermark detector</h1> | |
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#paramsModal"> | |
<i class="bi bi-gear"></i> | |
</button> | |
</div> | |
<!-- Advanced Parameters Modal --> | |
<div class="modal fade" id="paramsModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Advanced Parameters</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<div class="mb-3"> | |
<label for="detectorType" class="form-label">Detector Type</label> | |
<select class="form-select" id="detectorType"> | |
<option value="maryland">Maryland</option> | |
<option value="marylandz">Maryland Z-score</option> | |
<option value="openai">OpenAI</option> | |
<option value="openaiz">OpenAI Z-score</option> | |
</select> | |
<div class="form-text">Type of watermark detection algorithm</div> | |
</div> | |
<div class="mb-3"> | |
<label for="seed" class="form-label">Seed</label> | |
<input type="number" class="form-control" id="seed" value="0"> | |
<div class="form-text">Random seed for the watermark detector</div> | |
</div> | |
<div class="mb-3"> | |
<label for="ngram" class="form-label">N-gram Size</label> | |
<input type="number" class="form-control" id="ngram" value="1"> | |
<div class="form-text">Size of the n-gram window used for detection</div> | |
</div> | |
<div class="mb-3"> | |
<label for="delta" class="form-label">Delta</label> | |
<input type="number" step="0.1" class="form-control" id="delta" value="2.0"> | |
<div class="form-text">Bias added to greenlist tokens (for Maryland method)</div> | |
</div> | |
<div class="mb-3"> | |
<label for="temperature" class="form-label">Temperature</label> | |
<input type="number" step="0.1" class="form-control" id="temperature" value="0.8"> | |
<div class="form-text">Temperature for sampling (higher = more random)</div> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
<button type="button" class="btn btn-primary" id="applyParams">Apply</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Input Form --> | |
<div class="input-section"> | |
<div class="prompt-container"> | |
<textarea id="prompt_text" | |
placeholder="Enter your prompt here to generate text with the model..."></textarea> | |
<button class="floating-btn" id="generateBtn"> | |
<i class="bi bi-send-fill send-icon"></i> | |
<i class="bi bi-stop-fill stop-icon"></i> | |
</button> | |
</div> | |
<textarea id="user_text" | |
placeholder="Generated text will appear here. Replace or edit this text to see how watermark detection works."></textarea> | |
</div> | |
<!-- Token Display --> | |
<div class="token-display" id="tokenDisplay"></div> | |
<!-- Statistics --> | |
<div class="stats-container"> | |
<div> | |
<div class="stat-value" id="tokenCount">0</div> | |
<div class="stat-label"> | |
Tokens | |
<i class="bi bi-question-circle help-icon"></i> | |
<span class="help-tooltip">Total number of tokens in the text</span> | |
</div> | |
</div> | |
<div> | |
<div class="stat-value" id="scoredTokens">0</div> | |
<div class="stat-label"> | |
Scored Tokens | |
<i class="bi bi-question-circle help-icon"></i> | |
<span class="help-tooltip">Number of tokens that were actually scored by the detector (excludes first n-gram tokens and duplicates)</span> | |
</div> | |
</div> | |
<div> | |
<div class="stat-value" id="finalScore">0.00</div> | |
<div class="stat-label"> | |
Final Score | |
<i class="bi bi-question-circle help-icon"></i> | |
<span class="help-tooltip">Cumulative score from all scored tokens. Higher values indicate more likely watermarked text</span> | |
</div> | |
</div> | |
<div> | |
<div class="stat-value" id="pValue">0.500</div> | |
<div class="stat-label"> | |
P-value | |
<i class="bi bi-question-circle help-icon"></i> | |
<span class="help-tooltip">Statistical significance of the score. Lower values indicate stronger evidence of watermarking (p < 0.05 is typically considered significant)</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script> | |
let debounceTimeout = null; | |
let abortController = null; | |
const textarea = document.getElementById('user_text'); | |
const promptArea = document.getElementById('prompt_text'); | |
const generateBtn = document.getElementById('generateBtn'); | |
const tokenDisplay = document.getElementById('tokenDisplay'); | |
const tokenCount = document.getElementById('tokenCount'); | |
const scoredTokens = document.getElementById('scoredTokens'); | |
const finalScore = document.getElementById('finalScore'); | |
const pValue = document.getElementById('pValue'); | |
const applyParamsBtn = document.getElementById('applyParams'); | |
const seedInput = document.getElementById('seed'); | |
const ngramInput = document.getElementById('ngram'); | |
const detectorTypeSelect = document.getElementById('detectorType'); | |
const deltaInput = document.getElementById('delta'); | |
const temperatureInput = document.getElementById('temperature'); | |
function startGeneration() { | |
const prompt = promptArea.value.trim(); | |
if (!prompt) { | |
alert('Please enter a prompt first.'); | |
return; | |
} | |
generateBtn.classList.add('generating'); | |
textarea.value = ''; | |
// Create new AbortController for this request | |
abortController = new AbortController(); | |
// Get current parameters | |
const params = { | |
detector_type: detectorTypeSelect.value, | |
seed: parseInt(seedInput.value) || 0, | |
ngram: parseInt(ngramInput.value) || 1, | |
delta: parseFloat(deltaInput.value) || 2.0, | |
temperature: parseFloat(temperatureInput.value) || 0.8 | |
}; | |
// Create headers for SSE | |
const headers = new Headers({ | |
'Content-Type': 'application/json', | |
'Accept': 'text/event-stream', | |
}); | |
// Start fetch request with signal | |
fetch('/generate', { | |
method: 'POST', | |
headers: headers, | |
body: JSON.stringify({ | |
prompt: prompt, | |
params: params | |
}), | |
signal: abortController.signal // Add the abort signal | |
}).then(response => { | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
function processText(text) { | |
const lines = text.split('\n'); | |
for (const line of lines) { | |
if (line.startsWith('data: ')) { | |
try { | |
const data = JSON.parse(line.slice(6)); | |
if (data.error) { | |
alert('Error: ' + data.error); | |
stopGeneration(); | |
return; | |
} | |
if (data.token) { | |
// Append new token to existing text | |
textarea.value += data.token; | |
updateTokenization(); | |
} | |
if (data.text) { | |
// Final text (only used if something went wrong with streaming) | |
textarea.value = data.text; | |
updateTokenization(); | |
} | |
if (data.done) { | |
stopGeneration(); | |
} | |
} catch (e) { | |
console.error('Error parsing SSE data:', e); | |
} | |
} | |
} | |
} | |
function pump() { | |
return reader.read().then(({value, done}) => { | |
if (done) { | |
if (buffer.length > 0) { | |
processText(buffer); | |
} | |
return; | |
} | |
buffer += decoder.decode(value, {stream: true}); | |
const lines = buffer.split('\n\n'); | |
buffer = lines.pop(); | |
for (const line of lines) { | |
processText(line); | |
} | |
return pump(); | |
}); | |
} | |
return pump(); | |
}) | |
.catch(error => { | |
if (error.name === 'AbortError') { | |
console.log('Generation stopped by user'); | |
} else { | |
console.error('Error:', error); | |
alert('Error: Failed to generate text'); | |
} | |
}) | |
.finally(() => { | |
generateBtn.classList.remove('generating'); | |
abortController = null; | |
}); | |
} | |
function stopGeneration() { | |
if (abortController) { | |
abortController.abort(); | |
abortController = null; | |
} | |
generateBtn.classList.remove('generating'); | |
} | |
// Remove BOTH old event listeners and add just one new one | |
generateBtn.addEventListener('click', function(e) { | |
e.preventDefault(); // Prevent any double triggers | |
if (generateBtn.classList.contains('generating')) { | |
stopGeneration(); | |
} else { | |
startGeneration(); | |
} | |
}); | |
async function updateTokenization() { | |
const text = textarea.value; | |
try { | |
// Validate parameters before sending | |
const seed = parseInt(seedInput.value); | |
const ngram = parseInt(ngramInput.value); | |
const delta = parseFloat(deltaInput.value); | |
const temperature = parseFloat(temperatureInput.value); | |
const response = await fetch('/tokenize', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
text: text, | |
params: { | |
detector_type: detectorTypeSelect.value, | |
seed: isNaN(seed) ? 0 : seed, | |
ngram: isNaN(ngram) ? 1 : ngram, | |
delta: isNaN(delta) ? 2.0 : delta, | |
temperature: isNaN(temperature) ? 0.8 : temperature | |
} | |
}) | |
}); | |
if (!response.ok) { | |
const errorData = await response.json(); | |
throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
if (data.error) { | |
throw new Error(data.error); | |
} | |
// Update token display | |
tokenDisplay.innerHTML = data.tokens.map((token, i) => { | |
const score = data.scores[i]; | |
const pvalue = data.pvalues[i]; | |
const scoreDisplay = (score !== null && !isNaN(score)) ? score.toFixed(3) : 'N/A'; | |
const pvalueDisplay = (pvalue !== null && !isNaN(pvalue)) ? formatPValue(pvalue) : 'N/A'; | |
return `<span class="token" style="background-color: ${data.colors[i]}"> | |
${token} | |
<div class="token-tooltip"> | |
Score: ${scoreDisplay}<br> | |
P-value: ${pvalueDisplay} | |
</div> | |
</span>`; | |
}).join(''); | |
// Update counts and stats - safely handle null values | |
tokenCount.textContent = data.token_count || 0; | |
scoredTokens.textContent = data.ntoks_scored || 0; | |
finalScore.textContent = (data.final_score !== null && !isNaN(data.final_score)) ? | |
data.final_score.toFixed(2) : '0.00'; | |
pValue.textContent = (data.final_pvalue !== null && !isNaN(data.final_pvalue)) ? | |
formatPValue(data.final_pvalue) : '0.500'; | |
// Clear any previous error | |
const existingError = tokenDisplay.querySelector('.alert-danger'); | |
if (existingError) { | |
existingError.remove(); | |
} | |
} catch (error) { | |
console.error('Error updating tokenization:', error); | |
// Show detailed error to user | |
tokenDisplay.innerHTML = `<div class="alert alert-danger"> | |
<strong>Error:</strong> ${error.message || 'Error updating results. Please try again.'} | |
</div>`; | |
// Reset stats on error | |
tokenCount.textContent = '0'; | |
scoredTokens.textContent = '0'; | |
finalScore.textContent = '0.00'; | |
pValue.textContent = '0.500'; | |
} | |
} | |
// Increase debounce timeout and ensure it's properly cleared | |
textarea.addEventListener('input', function() { | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); // Increased to 500ms | |
}); | |
// Add input event listeners for parameter fields to trigger updates | |
seedInput.addEventListener('input', function() { | |
const value = this.value === '' ? '' : parseInt(this.value); | |
if (isNaN(value) && this.value !== '') { | |
this.value = "0"; | |
} | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); | |
}); | |
ngramInput.addEventListener('input', function() { | |
const value = this.value === '' ? '' : parseInt(this.value); | |
if (isNaN(value) && this.value !== '') { | |
this.value = "1"; | |
} | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); | |
}); | |
deltaInput.addEventListener('input', function() { | |
const value = this.value === '' ? '' : parseFloat(this.value); | |
if (isNaN(value) && this.value !== '') { | |
this.value = "2.0"; | |
} | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); | |
}); | |
temperatureInput.addEventListener('input', function() { | |
const value = this.value === '' ? '' : parseFloat(this.value); | |
if (isNaN(value) && this.value !== '') { | |
this.value = "0.8"; | |
} | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); | |
}); | |
// Add keyboard shortcut for applying changes | |
document.addEventListener('keydown', function(e) { | |
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { | |
e.preventDefault(); | |
if (document.activeElement === promptArea) { | |
if (generateBtn.classList.contains('generating')) { | |
stopGeneration(); | |
} else { | |
startGeneration(); | |
} | |
} else { | |
applyParamsBtn.click(); | |
} | |
} | |
}); | |
detectorTypeSelect.addEventListener('change', function() { | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
debounceTimeout = setTimeout(updateTokenization, 500); | |
}); | |
// Ensure the modal apply button properly triggers an update | |
applyParamsBtn.addEventListener('click', function() { | |
updateTokenization().then(() => { | |
const modal = bootstrap.Modal.getInstance(document.getElementById('paramsModal')); | |
if (modal) { | |
modal.hide(); | |
} | |
}).catch(error => { | |
console.error('Error applying parameters:', error); | |
}); | |
}); | |
// Initial tokenization with error handling | |
document.addEventListener('DOMContentLoaded', function() { | |
updateTokenization().catch(error => { | |
console.error('Error during initial tokenization:', error); | |
}); | |
}); | |
// Add this helper function for formatting p-values | |
function formatPValue(value) { | |
if (value >= 0.001) { | |
return value.toFixed(3); | |
} else { | |
return value.toExponential(2); | |
} | |
} | |
</script> | |
</body> | |
</html> | |