diff --git a/api/redact-download.php b/api/redact-download.php
new file mode 100644
index 0000000..0b77db5
--- /dev/null
+++ b/api/redact-download.php
@@ -0,0 +1,129 @@
+ ['message' => 'ZipArchive extension not available.']]);
+ exit;
+}
+
+$docx = buildMinimalDocx($text);
+
+header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+header('Content-Disposition: attachment; filename="redacted.docx"');
+header('Content-Length: ' . strlen($docx));
+header('Cache-Control: no-store');
+echo $docx;
+exit;
+
+function buildMinimalDocx(string $text): string
+{
+ $tmp = tempnam(sys_get_temp_dir(), 'dbn_docx_');
+ @unlink($tmp);
+ $tmp .= '.docx';
+
+ $zip = new ZipArchive();
+ $zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+
+ $zip->addFromString('[Content_Types].xml', contentTypesXml());
+ $zip->addFromString('_rels/.rels', relsXml());
+ $zip->addFromString('word/document.xml', documentXml($text));
+ $zip->addFromString('word/_rels/document.xml.rels', wordRelsXml());
+ $zip->addFromString('docProps/app.xml', appXml());
+ $zip->addFromString('docProps/core.xml', coreXml());
+
+ $zip->close();
+
+ $bytes = file_get_contents($tmp);
+ @unlink($tmp);
+ return $bytes;
+}
+
+function documentXml(string $text): string
+{
+ $lines = explode("\n", str_replace("\r\n", "\n", str_replace("\r", "\n", $text)));
+ $paras = [];
+ foreach ($lines as $line) {
+ $safe = htmlspecialchars($line, ENT_XML1 | ENT_COMPAT, 'UTF-8');
+ if ($safe === '') {
+ $paras[] = '
${escapeHtml(data.answer || data.what_we_found || '')}
`; } if (data.tool === 'redact') { - return `${escapeHtml(data.redacted_text || '')}${renderEntityCounts(data.entity_counts)}`;
+ lastRedactedText = data.redacted_text || '';
+ const t = (k) => currentRedactT(k) || k;
+ const dlRow = `${escapeHtml(lastRedactedText)}${renderEntityCounts(data.entity_counts)}${dlRow}`;
}
if (data.tool === 'timeline') {
lastTimelineEvents = data.events || [];
@@ -1459,6 +1490,42 @@ function downloadTranscriptVtt() {
downloadBlob(new Blob([lines.join('\n')], { type: 'text/vtt' }), 'transcript.vtt');
}
+async function copyRedactedText() {
+ if (!lastRedactedText) return;
+ const btn = document.getElementById('rdlCopy');
+ await navigator.clipboard.writeText(lastRedactedText);
+ if (btn) {
+ const orig = btn.textContent;
+ btn.textContent = currentRedactT('redactCopied') || 'Copied!';
+ setTimeout(() => { btn.textContent = orig; }, 1800);
+ }
+}
+
+function downloadRedactedTxt() {
+ if (!lastRedactedText) return;
+ downloadBlob(new Blob([lastRedactedText], { type: 'text/plain' }), 'redacted.txt');
+}
+
+async function downloadRedactedDocx() {
+ if (!lastRedactedText) return;
+ const btn = document.getElementById('rdlDocx');
+ if (btn) btn.disabled = true;
+ try {
+ const resp = await fetch('/api/redact-download.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: lastRedactedText, format: 'docx' }),
+ });
+ if (!resp.ok) throw new Error('Download failed');
+ const blob = await resp.blob();
+ downloadBlob(blob, 'redacted.docx');
+ } catch (e) {
+ alert(e.message);
+ } finally {
+ if (btn) btn.disabled = false;
+ }
+}
+
function resetAudio() {
audioQueue = [];
if (!els.audioInput) return;
diff --git a/redact.php b/redact.php
index 81fdd5c..3f5b6c5 100644
--- a/redact.php
+++ b/redact.php
@@ -24,61 +24,66 @@ require_once __DIR__ . '/includes/layout.php';
Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy. Regex-only is instant and free but finds no names or organisations.
-Standard: regex patterns + LLM scan for names/orgs/places. Strict: also replaces any capitalised two-word phrase as a potential name — more aggressive, may produce false positives.
+Nordic: Norwegian fødselsnummer, phone, email, addresses. European: adds IBAN, SE personnummer, UK NI. ECHR: adds application numbers, DOB phrases. Global: adds US SSN, document numbers.
- -When checked, judges, expert witnesses and caseworkers keep their names in a labelled tag: [JUDGE: Andersen]. Uncheck to replace all names with generic role tags.
- -Contextual: each person gets a role tag so their identity is traceable within the document. Generic: all names become [PERSON]. Pseudonyms: replaced with plausible fake Norwegian values.
- -Names listed here will never be redacted, even if the AI would otherwise remove them — e.g. a judge or expert who must remain identifiable.
-Standard: regex patterns + LLM scan for names/orgs/places. Strict: also replaces any capitalised two-word phrase as a potential name — more aggressive, may produce false positives.
-Replace a specific name with a custom bracketed label, e.g. “David Jr” → [Junior].
-Nordic: Norwegian fødselsnummer, phone, email, addresses. European: adds IBAN, SE personnummer, UK NI. ECHR: adds application numbers, DOB phrases. Global: adds US SSN, document numbers.
+ +When checked, judges, expert witnesses and caseworkers keep their names in a labelled tag: [JUDGE: Andersen]. Uncheck to replace all names with generic role tags.
+ +Contextual: each person gets a role tag so their identity is traceable within the document. Generic: all names become [PERSON]. Pseudonyms: replaced with plausible fake Norwegian values.
+ +Names listed here will never be redacted, even if the AI would otherwise remove them — e.g. a judge or expert who must remain identifiable.
+Replace a specific name with a custom bracketed label, e.g. “David Jr” → [Junior].
+