Manage station audio sequences visually. (Supports multiple IDs separated by /)
Manage station audio sequences visually. (Supports multiple IDs separated by /)
-
+
@@ -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 = `
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 = `
Add Line`;
+ }
+
+ document.getElementById('list-container').innerHTML = '
Loading...
';
+ 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
All ${importCount} items imported successfully!`;
// Reset UI after 2 seconds
@@ -1153,14 +1191,14 @@
showToast("Error", e.message);
} finally {
btn.disabled = false;
- btn.textContent = "Generate Audio";
+ btn.innerHTML = `
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 = `
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 = '
Failed to load list.txt
';
+ document.getElementById('list-container').innerHTML = `
Failed to load ${currentMode === 'station' ? 'list.txt' : 'line.txt'}
`;
}
}
@@ -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 = `
Save to Server`;
}
}
diff --git a/CloudServer/server.js b/CloudServer/server.js
index 2f3a4e1..cac7ec3 100644
--- a/CloudServer/server.js
+++ b/CloudServer/server.js
@@ -1763,52 +1763,109 @@ function findStationByFlexibleName(targetName) {
function handleTrainArrival(ws, data) {
const trainId = data.trainId;
const stationName = getCorrectStationName(data.stationName);
-
- const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
+ const stationCode = data.stationCode;
+ const platform = data.platform || '1';
+ const lineCode = data.lineCode || 'UNK';
+ const destCode = data.destCode || 'UNK';
+ const nextCode = data.nextCode || 'UNK';
+
+ const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
const trainData = {
trainId,
stationName,
status: 'arrived',
lastUpdated: Date.now()
};
-
+
if (existingIndex >= 0) {
activeTrains[existingIndex] = trainData;
} else {
activeTrains.push(trainData);
}
-
- log('INFO', `Train ${trainId} arrived at ${stationName}`);
+
+ log('INFO', `Train ${trainId} arrived at ${stationName}. Broadcasting...`);
saveData();
broadcastUpdate();
-
+
ws.send(JSON.stringify({
type: 'train_arrival_ack',
success: true,
message: 'Train arrival processed successfully',
trainId
}));
+
+ // Broadcast Audio Logic
+ const seq = [
+ 'ksts',
+ `ja-${platform}`, 'ja-zt', `ja-${lineCode}`, 'ja-xl', `ja-${destCode}`, 'ja-to', 'ja-nextsta', `ja-${nextCode}`, 'ja-warn',
+ `ch-${platform}`, 'ch-zt', `ch-${lineCode}`, 'ch-line', 'ch-to', `ch-${destCode}`, 'ch-nextsta', `ch-${nextCode}`, 'ch-warn'
+ ];
+
+ enqueueHlsPush(stationName, seq);
+
+ const audioId = `arrival-${stationCode}-${Date.now()}`;
+ const built = buildSequenceWavFile(seq);
+ if (built) {
+ enqueueStationAudioTask(stationName, ws, {
+ audioFormat: built.audioFormat,
+ streamUrl: built.streamUrl,
+ durationSec: built.durationSec,
+ priority: 1,
+ interruptMode: 'ifLower',
+ source: 'train_arrival',
+ audioId,
+ consoleStreamStart: { type: 'stream_start', station: stationName, seq, streamName: getStationStreamName(stationName), hlsUrl: getStationHlsUrl(getStationStreamName(stationName)) },
+ consoleStreamEnd: { type: 'stream_end', station: stationName }
+ });
+ }
}
function handleTrainDeparture(ws, data) {
const trainId = data.trainId;
const stationName = getCorrectStationName(data.stationName);
-
- const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
+ const stationCode = data.stationCode;
+ const beep = data.beep || 'fc1';
+
+ const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
if (existingIndex >= 0) {
activeTrains.splice(existingIndex, 1);
}
-
- log('INFO', `Train ${trainId} departed from ${stationName}`);
+
+ log('INFO', `Train ${trainId} departing from ${stationName}. Broadcasting departure...`);
saveData();
broadcastUpdate();
-
+
ws.send(JSON.stringify({
type: 'train_departure_ack',
success: true,
message: 'Train departure processed successfully',
trainId
}));
+
+ // Departure Audio Logic
+ const seq = [
+ beep,
+ 'ja-doorwarn',
+ 'ch-doorwarn'
+ ];
+
+ enqueueHlsPush(stationName, seq);
+
+ const audioId = `departure-${stationCode}-${Date.now()}`;
+ const built = buildSequenceWavFile(seq);
+ if (built) {
+ enqueueStationAudioTask(stationName, ws, {
+ audioFormat: built.audioFormat,
+ streamUrl: built.streamUrl,
+ durationSec: built.durationSec,
+ priority: 1,
+ interruptMode: 'ifLower',
+ source: 'train_departure',
+ audioId,
+ consoleStreamStart: { type: 'stream_start', station: stationName, seq, streamName: getStationStreamName(stationName), hlsUrl: getStationHlsUrl(getStationStreamName(stationName)) },
+ consoleStreamEnd: { type: 'stream_end', station: stationName }
+ });
+ }
}
function handleStationTimeRequest(ws, data) {
@@ -2460,6 +2517,7 @@ app.post('/api/tts/upload_temp', upload.fields([{ name: 'zhAudio', maxCount: 1 }
// Import TTS temp files to station folder
app.post('/api/tts/import', express.json(), (req, res) => {
const stationCode = String(req.body?.stationCode || '').trim();
+ const type = String(req.body?.type || 'station').trim();
const zhTemp = String(req.body?.zhTemp || '').trim();
const jaTemp = String(req.body?.jaTemp || req.body?.enTemp || '').trim();
if (!stationCode || !zhTemp || !jaTemp) return res.status(400).json({ success: false, message: 'Missing parameters' });
@@ -2467,8 +2525,10 @@ app.post('/api/tts/import', express.json(), (req, res) => {
const zhSource = path.join(uploadDir, zhTemp);
const jaSource = path.join(uploadDir, jaTemp);
const safeStationCode = String(stationCode).replace(/\//g, '-');
- const zhDest = path.join(audioStationDir, `ch-${safeStationCode}.wav`);
- const jaDest = path.join(audioStationDir, `ja-${safeStationCode}.wav`);
+
+ const targetDir = type === 'line' ? audioGeneralDir : audioStationDir;
+ const zhDest = path.join(targetDir, `ch-${safeStationCode}.wav`);
+ const jaDest = path.join(targetDir, `ja-${safeStationCode}.wav`);
try {
if (fs.existsSync(zhSource)) {