feat: metadati AI detection + prompt/lyrics + pannello frame grezzi (v1.13.0)
Metadati tab estesa per audit e cleanup file AI-generated: - Nuovi campi read/write: source_url (WOAS ID3), lyrics (USLT), plus \xa9lyr per M4A - Badge AI con riconoscimento: Suno, Udio, Riffusion, Mubert, AIVA, Boomy, SoundRAW, Loudly + presenza C2PA manifest (GEOB frame) - Pannello "Metadati avanzati" collapsable: tabella di tutti i frame ID3 grezzi (chiave, tipo text/binary, preview, size) - Rimozione selettiva frame custom via bridge remove_metadata_frames: ID3.__delitem__() + save() per MP3, MP4.tags per M4A. Frame standard protetti (TIT2/TPE1/APIC ecc.) marcati 🔒 nella UI, si svuotano dai campi form - Rilevamento AI riusabile via core.metadata._detect_ai(comment, source_url, extra) — case-insensitive, returns (is_ai, source_name) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
8432b2a592
commit
9644ffec35
6 files changed
+399
-1
No files matched your search
@@ -578,6 +578,87 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
.path-display:empty,
|
||||
.path-display.empty { color: var(--text-3); }
|
||||
|
||||
.ai-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(135deg, var(--purple), var(--pink));
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(168, 85, 247, 0.35);
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.ai-badge[hidden] { display: none !important; }
|
||||
.ai-badge::before { content: "🤖"; }
|
||||
|
||||
.raw-frames-details {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg-card-2);
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.raw-frames-details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
padding: 2px 0;
|
||||
user-select: none;
|
||||
}
|
||||
.raw-frames-details summary:hover { color: var(--text); }
|
||||
.raw-frames-details[open] summary { margin-bottom: 10px; color: var(--text); }
|
||||
.raw-frames-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.raw-frames-table th,
|
||||
.raw-frames-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.raw-frames-table th {
|
||||
color: var(--text-3);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.raw-frames-table td.raw-key {
|
||||
font-family: ui-monospace, SF Mono, Menlo, monospace;
|
||||
color: var(--indigo-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.raw-frames-table td.raw-preview {
|
||||
word-break: break-word;
|
||||
color: var(--text);
|
||||
}
|
||||
.raw-frames-table td.raw-size {
|
||||
text-align: right;
|
||||
color: var(--text-3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.raw-frames-table .kind-binary {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: var(--amber-dim);
|
||||
color: var(--amber-text);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
BUTTONS
|
||||
=============================== */
|
||||
|
||||
@@ -444,6 +444,7 @@
|
||||
<div class="row">
|
||||
<button class="btn btn-amber pill" id="metaPickBtn">📂 Apri file audio</button>
|
||||
<div class="path-display" id="metaPathDisplay">Nessun file selezionato</div>
|
||||
<span id="metaAiBadge" class="ai-badge" hidden title="Brano generato con AI"></span>
|
||||
</div>
|
||||
<div class="meta-info" id="metaInfo" hidden></div>
|
||||
</div>
|
||||
@@ -492,6 +493,42 @@
|
||||
<label class="field-label">Commento</label>
|
||||
<textarea id="metaComment" class="input textarea" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label class="field-label">URL sorgente (WOAS)</label>
|
||||
<input type="text" id="metaSourceUrl" class="input pill" placeholder="https://...">
|
||||
<div class="cover-hint">Link alla pagina d'origine del brano (es. Suno, YouTube, SoundCloud). Sopravvive alle copie del file.</div>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label class="field-label">Testo / Prompt (USLT)</label>
|
||||
<textarea id="metaLyrics" class="input textarea" rows="8" placeholder="Lyrics del brano o prompt AI utilizzato..."></textarea>
|
||||
<div class="cover-hint">Testo o prompt strutturato. Suno lo compila automaticamente al momento della generazione.</div>
|
||||
</div>
|
||||
<details class="field full raw-frames-details">
|
||||
<summary>Metadati avanzati (tutti i frame ID3 grezzi)</summary>
|
||||
<table class="raw-frames-table" id="metaRawFrames">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32px;"><input type="checkbox" id="metaRawSelectAll" title="Seleziona tutti"></th>
|
||||
<th>Frame</th>
|
||||
<th>Tipo</th>
|
||||
<th>Contenuto</th>
|
||||
<th style="text-align:right;">Bytes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="row" style="margin-top:10px;justify-content:space-between;align-items:center;">
|
||||
<span id="metaRawSelectedCount" style="font-size:12px;color:var(--text-3);">0 selezionati</span>
|
||||
<button id="metaRawRemoveBtn" class="btn btn-danger pill btn-sm" disabled>
|
||||
🗑 Rimuovi selezionati
|
||||
</button>
|
||||
</div>
|
||||
<div class="cover-hint" style="margin-top:8px;">
|
||||
I frame standard (TIT2 titolo, TPE1 artista, TALB album, APIC cover…) si gestiscono con i campi sopra.
|
||||
Qui rimuovi frame custom come TXXX, GEOB (C2PA), USLT (lyrics) o COMM extra.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="field full" id="metaWhereFromField" hidden>
|
||||
<label class="field-label">
|
||||
Origine (macOS · kMDItemWhereFroms)
|
||||
|
||||
@@ -1147,6 +1147,51 @@ function populateMetaForm(d) {
|
||||
$("#metaBpm").value = d.bpm || "";
|
||||
$("#metaKey").value = d.key || "";
|
||||
$("#metaComment").value = d.comment || "";
|
||||
$("#metaSourceUrl").value = d.source_url || "";
|
||||
$("#metaLyrics").value = d.lyrics || "";
|
||||
|
||||
// Raw frames table
|
||||
const rawBody = document.querySelector("#metaRawFrames tbody");
|
||||
if (rawBody) {
|
||||
// Frame gestiti dai campi standard: no checkbox (li rimuovi svuotando il campo)
|
||||
const PROTECTED_MP3 = /^(TIT2|TPE1|TPE2|TALB|TDRC|TRCK|TCON|TBPM|TKEY|APIC)/;
|
||||
const PROTECTED_MP4 = new Set(["\xa9nam","\xa9ART","aART","\xa9alb","\xa9day","trkn","\xa9gen","tmpo","covr"]);
|
||||
rawBody.innerHTML = "";
|
||||
(d.raw_frames || []).forEach((f) => {
|
||||
const tr = document.createElement("tr");
|
||||
const kindHtml = f.kind === "binary"
|
||||
? '<span class="kind-binary">binary</span>'
|
||||
: '<span style="color:var(--text-3);font-size:11px;">text</span>';
|
||||
const isProtected = PROTECTED_MP3.test(f.key) || PROTECTED_MP4.has(f.key);
|
||||
const cbHtml = isProtected
|
||||
? '<span title="Frame standard — gestito dai campi sopra" style="color:var(--text-3);font-size:14px;">🔒</span>'
|
||||
: `<input type="checkbox" class="meta-raw-check" data-key="${_escapeHtml(f.key)}">`;
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">${cbHtml}</td>
|
||||
<td class="raw-key">${_escapeHtml(f.key)}</td>
|
||||
<td>${kindHtml}</td>
|
||||
<td class="raw-preview">${_escapeHtml(f.preview || "")}</td>
|
||||
<td class="raw-size">${(f.size || 0).toLocaleString()}</td>
|
||||
`;
|
||||
rawBody.appendChild(tr);
|
||||
});
|
||||
// Reset stato selezione
|
||||
const master = $("#metaRawSelectAll");
|
||||
if (master) { master.checked = false; master.indeterminate = false; }
|
||||
_updateRawFramesSelection();
|
||||
}
|
||||
|
||||
// AI badge
|
||||
const aiBadge = $("#metaAiBadge");
|
||||
if (aiBadge) {
|
||||
if (d.ai_generated) {
|
||||
aiBadge.hidden = false;
|
||||
aiBadge.textContent = d.ai_source ? `AI · ${d.ai_source}` : "AI Generated";
|
||||
} else {
|
||||
aiBadge.hidden = true;
|
||||
aiBadge.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
// WhereFroms (macOS only)
|
||||
if (d.is_macos) {
|
||||
@@ -1204,6 +1249,51 @@ $("#metaPickBtn").addEventListener("click", async () => {
|
||||
if (path) await loadMetaFile(path);
|
||||
});
|
||||
|
||||
function _updateRawFramesSelection() {
|
||||
const boxes = $$("#metaRawFrames .meta-raw-check");
|
||||
const checked = boxes.filter((b) => b.checked);
|
||||
const cnt = $("#metaRawSelectedCount");
|
||||
if (cnt) cnt.textContent = `${checked.length} selezionati`;
|
||||
const btn = $("#metaRawRemoveBtn");
|
||||
if (btn) btn.disabled = checked.length === 0;
|
||||
const master = $("#metaRawSelectAll");
|
||||
if (master) {
|
||||
if (boxes.length === 0) { master.checked = false; master.indeterminate = false; }
|
||||
else if (checked.length === 0) { master.checked = false; master.indeterminate = false; }
|
||||
else if (checked.length === boxes.length) { master.checked = true; master.indeterminate = false; }
|
||||
else { master.checked = false; master.indeterminate = true; }
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("change", (e) => {
|
||||
const t = e.target;
|
||||
if (t && t.classList && t.classList.contains("meta-raw-check")) {
|
||||
_updateRawFramesSelection();
|
||||
}
|
||||
});
|
||||
|
||||
$("#metaRawSelectAll")?.addEventListener("change", (e) => {
|
||||
$$("#metaRawFrames .meta-raw-check").forEach((b) => { b.checked = e.target.checked; });
|
||||
_updateRawFramesSelection();
|
||||
});
|
||||
|
||||
$("#metaRawRemoveBtn")?.addEventListener("click", async () => {
|
||||
const keys = $$("#metaRawFrames .meta-raw-check")
|
||||
.filter((b) => b.checked)
|
||||
.map((b) => b.dataset.key);
|
||||
if (keys.length === 0) return;
|
||||
if (!window.confirm(`Rimuovere ${keys.length} frame dal file?\n\n${keys.join("\n")}\n\nOperazione permanente.`)) return;
|
||||
const path = state.meta?.path;
|
||||
if (!path) return;
|
||||
const res = await window.pywebview.api.remove_metadata_frames(path, keys);
|
||||
if (res && res.ok) {
|
||||
toast(`Rimossi ${res.removed || 0} frame`, "ok");
|
||||
await loadMetaFile(path); // ricarica per aggiornare tabella
|
||||
} else {
|
||||
toast("Errore: " + (res?.error || "sconosciuto"), "error");
|
||||
}
|
||||
});
|
||||
|
||||
$("#metaCoverBtn").addEventListener("click", async () => {
|
||||
const path = await window.pywebview.api.pick_image_file();
|
||||
if (!path) return;
|
||||
@@ -1254,6 +1344,8 @@ $("#metaSaveBtn").addEventListener("click", async () => {
|
||||
bpm: $("#metaBpm").value,
|
||||
key: $("#metaKey").value,
|
||||
comment: $("#metaComment").value,
|
||||
source_url: $("#metaSourceUrl").value.trim(),
|
||||
lyrics: $("#metaLyrics").value,
|
||||
where_from: $("#metaWhereFrom").value
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
|
||||
Reference in new issue
Block a user