feat: Add Line Audio Generation and Line List Manager

This commit is contained in:
2026-08-01 11:20:58 +08:00
parent b140aafa2d
commit e66f39284f
2 changed files with 145 additions and 42 deletions
+72 -29
View File
@@ -533,10 +533,16 @@
<div class="layout-wrapper">
<div class="header">
<h2>Broadcast Generator & Manager</h2>
<a href="/" class="btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back to Console
</a>
<div style="display: flex; gap: 12px; align-items: center;">
<div style="display: flex; background: var(--bg-input); border: 1px solid var(--bg-border); border-radius: 6px; padding: 4px;">
<button class="btn btn-primary" id="mode-station" onclick="switchMode('station')" style="border: none; padding: 6px 16px;">Station</button>
<button class="btn" id="mode-line" onclick="switchMode('line')" style="border: none; padding: 6px 16px;">Line</button>
</div>
<a href="/" class="btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back to Console
</a>
</div>
</div>
<div class="container">
@@ -561,12 +567,12 @@
<!-- Single Generation View -->
<div id="view-single" style="display: flex; flex-direction: column; gap: 20px;">
<div class="form-group">
<label>Chinese Station Name (e.g., 北京)</label>
<label id="lbl-zh-name">Chinese Station Name (e.g., 北京)</label>
<input type="text" id="zh-text" placeholder="输入中文站名">
</div>
<div class="form-group">
<label>Japanese Station Name (e.g., ちば)</label>
<label id="lbl-en-name">Japanese Station Name (e.g., ちば)</label>
<input type="text" id="en-text" placeholder="输入日文站名">
</div>
@@ -591,7 +597,7 @@
</div>
<div class="form-group" style="margin-top:12px; padding-top:20px; border-top:1px solid var(--bg-border)">
<label>Station Code (e.g., 01-01)</label>
<label id="lbl-station-code">Station Code (e.g., 01-01)</label>
<input type="text" id="station-code" placeholder="01-01">
</div>
@@ -642,13 +648,13 @@
<div class="panel">
<div class="panel-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
Manage list.txt
<span id="manage-title">Manage list.txt</span>
</div>
<div style="display:flex; justify-content:space-between; align-items: flex-end; margin-bottom: 8px;">
<div class="panel-subtitle" style="margin:0; border:none; padding:0;">Manage station audio sequences visually. (Supports multiple IDs separated by /)</div>
<div class="panel-subtitle" id="manage-subtitle" style="margin:0; border:none; padding:0;">Manage station audio sequences visually. (Supports multiple IDs separated by /)</div>
<div style="display:flex; gap:10px; flex-shrink: 0;">
<button class="btn" onclick="openAddModal()" style="white-space: nowrap;">
<button class="btn" id="btn-add-station" onclick="openAddModal()" style="white-space: nowrap;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add Station
</button>
@@ -681,7 +687,7 @@
</div>
<div class="modal-body">
<div class="form-group">
<label>Station Code</label>
<label id="lbl-edit-code">Station Code</label>
<input type="text" id="edit-code" placeholder="e.g. 01-01 / 01-01/01-02">
</div>
@@ -723,6 +729,7 @@
const ZH_REFERENCE_ID = "0aee9f0f5683480d987c9e3c5d2de04c";
const JA_REFERENCE_ID = "38985e545cff41b79c11c32b0196c9a0";
let currentMode = 'station'; // 'station' or 'line'
let zhTempFile = null;
let jaTempFile = null;
@@ -732,6 +739,37 @@
let currentSequence = [];
let selectedFileValue = '';
async function switchMode(mode) {
currentMode = mode;
const btnStation = document.getElementById('mode-station');
const btnLine = document.getElementById('mode-line');
if (mode === 'station') {
btnStation.className = 'btn btn-primary';
btnLine.className = 'btn';
document.getElementById('lbl-zh-name').textContent = 'Chinese Station Name (e.g., 北京)';
document.getElementById('lbl-en-name').textContent = 'Japanese Station Name (e.g., ちば)';
document.getElementById('lbl-station-code').textContent = 'Station Code (e.g., 01-01)';
document.getElementById('lbl-edit-code').textContent = 'Station Code';
document.getElementById('manage-title').textContent = 'Manage list.txt';
document.getElementById('manage-subtitle').textContent = 'Manage station audio sequences visually. (Supports multiple IDs separated by /)';
document.getElementById('btn-add-station').innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> Add Station`;
} else {
btnStation.className = 'btn';
btnLine.className = 'btn btn-primary';
document.getElementById('lbl-zh-name').textContent = 'Chinese Line Name (e.g., 一号线)';
document.getElementById('lbl-en-name').textContent = 'Japanese Line Name (e.g., 1ごうせん)';
document.getElementById('lbl-station-code').textContent = 'Line Code (e.g., line1)';
document.getElementById('lbl-edit-code').textContent = 'Line Code';
document.getElementById('manage-title').textContent = 'Manage line.txt';
document.getElementById('manage-subtitle').textContent = 'Manage line audio sequences visually.';
document.getElementById('btn-add-station').innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> Add Line`;
}
document.getElementById('list-container').innerHTML = '<div style="text-align:center; padding: 40px; color:var(--text-muted)">Loading...</div>';
await loadList();
}
function toggleCustomSelect() {
const trigger = document.querySelector('#custom-file-select .custom-select-trigger');
const options = document.getElementById('custom-select-options');
@@ -833,7 +871,7 @@
const res = await fetch('/api/tts/import', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ stationCode: code, zhTemp, jaTemp })
body: JSON.stringify({ stationCode: code, zhTemp, jaTemp, type: currentMode })
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
@@ -934,7 +972,7 @@
btn.disabled = true;
btnCancel.style.display = 'none'; // Prevent cancel during generation
statusBox.style.display = 'block';
statusBox.innerHTML = `Starting batch generation for ${batchTasks.length} stations...\n`;
statusBox.innerHTML = `Starting batch generation for ${batchTasks.length} ${currentMode === 'station' ? 'stations' : 'lines'}...\n`;
let successCount = 0;
let failCount = 0;
@@ -1070,7 +1108,7 @@
if (importCount > 0) {
renderList();
saveList();
showToast("Batch Complete", `${importCount} stations imported and added to list`);
showToast("Batch Complete", `${importCount} ${currentMode === 'station' ? 'stations' : 'lines'} imported and added to list`);
statusBox.innerHTML += `\n<b>All ${importCount} items imported successfully!</b>`;
// Reset UI after 2 seconds
@@ -1153,14 +1191,14 @@
showToast("Error", e.message);
} finally {
btn.disabled = false;
btn.textContent = "Generate Audio";
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> Generate Audio`;
}
}
async function importAudio() {
let code = document.getElementById('station-code').value.trim();
if (!code) {
showToast("Error", "Please enter a station code");
showToast("Error", `Please enter a ${currentMode === 'station' ? 'station' : 'line'} code`);
return;
}
@@ -1186,14 +1224,15 @@
body: JSON.stringify({
stationCode: code,
zhTemp: zhTempFile,
jaTemp: jaTempFile
jaTemp: jaTempFile,
type: currentMode
})
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
showToast("Success", "Files imported to Station folder!");
showToast("Success", `Files imported to ${currentMode === 'station' ? 'Station' : 'General'} folder!`);
// Automatically add/update to listData if generated via this panel
const filesStr = `ch-${code}.wav,ja-${code}.wav`;
@@ -1219,7 +1258,7 @@
showToast("Error", "Import failed: " + e.message);
} finally {
btn.disabled = false;
btn.textContent = "Import to Station Folder & Add to List";
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Import & Add to List`;
}
}
@@ -1254,7 +1293,8 @@
async function loadList() {
try {
const res = await fetch('/api/files/content?path=list.txt');
const targetPath = currentMode === 'station' ? 'list.txt' : 'Audio/general/line.txt';
const res = await fetch(`/api/files/content?path=${encodeURIComponent(targetPath)}`);
const data = await res.json();
listData = [];
if (data.success && data.content) {
@@ -1270,7 +1310,7 @@
}
renderList();
} catch (e) {
document.getElementById('list-container').innerHTML = '<div style="color:var(--danger)">Failed to load list.txt</div>';
document.getElementById('list-container').innerHTML = `<div style="color:var(--danger)">Failed to load ${currentMode === 'station' ? 'list.txt' : 'line.txt'}</div>`;
}
}
@@ -1305,7 +1345,8 @@
}
function removeListItem(index) {
if (confirm(`Delete station ${listData[index].code}?`)) {
const typeName = currentMode === 'station' ? 'station' : 'line';
if (confirm(`Delete ${typeName} ${listData[index].code}?`)) {
listData.splice(index, 1);
renderList();
}
@@ -1315,7 +1356,7 @@
function openAddModal() {
editingIndex = -1;
currentSequence = [];
document.getElementById('modal-title').textContent = 'Add Station Configuration';
document.getElementById('modal-title').textContent = currentMode === 'station' ? 'Add Station Configuration' : 'Add Line Configuration';
document.getElementById('edit-code').value = '';
renderSequence();
selectCustomOption('', '-- Select an audio file --');
@@ -1326,7 +1367,7 @@
editingIndex = index;
const item = listData[index];
currentSequence = [...item.files];
document.getElementById('modal-title').textContent = `Edit Station: ${item.code}`;
document.getElementById('modal-title').textContent = currentMode === 'station' ? `Edit Station: ${item.code}` : `Edit Line: ${item.code}`;
document.getElementById('edit-code').value = item.code;
renderSequence();
selectCustomOption('', '-- Select an audio file --');
@@ -1386,14 +1427,15 @@
function confirmEdit() {
const code = document.getElementById('edit-code').value.trim();
const typeName = currentMode === 'station' ? 'Station' : 'Line';
if (!code) {
alert("Station code is required");
alert(`${typeName} code is required`);
return;
}
if (editingIndex === -1) {
if (listData.some(item => item.code === code)) {
alert(`Station ${code} already exists!`);
alert(`${typeName} ${code} already exists!`);
return;
}
listData.push({ code, files: [...currentSequence] });
@@ -1417,17 +1459,18 @@
.join('\n');
try {
const targetPath = currentMode === 'station' ? 'list.txt' : 'Audio/general/line.txt';
const res = await fetch('/api/files/content', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
path: 'list.txt',
path: targetPath,
content: content + '\n'
})
});
const data = await res.json();
if (data.success) {
showToast("Success", "list.txt saved successfully!");
showToast("Success", `${currentMode === 'station' ? 'list.txt' : 'line.txt'} saved successfully!`);
} else {
throw new Error(data.message);
}
@@ -1435,7 +1478,7 @@
showToast("Error", "Save failed: " + e.message);
} finally {
btn.disabled = false;
btn.textContent = "Save to Server";
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg> Save to Server`;
}
}