fix(tts): rename japanese station audio suffix to ja

Switch generated station audio entries from en-* to ja-* and update the generator flow so Chinese plus Japanese audio imports use the new suffix consistently.
This commit is contained in:
2026-08-01 09:02:18 +08:00
parent a2408da417
commit f7de7717dc
2 changed files with 57 additions and 52 deletions
+39 -39
View File
@@ -566,8 +566,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>English Station Name (e.g., Beijing)</label> <label>Japanese Station Name (e.g., ちば)</label>
<input type="text" id="en-text" placeholder="Enter English Name"> <input type="text" id="en-text" placeholder="输入日文站名">
</div> </div>
<button class="btn btn-primary" id="btn-generate" onclick="generateAudio()"> <button class="btn btn-primary" id="btn-generate" onclick="generateAudio()">
@@ -584,7 +584,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>English Preview</label> <label>Japanese Preview</label>
<div class="audio-preview"> <div class="audio-preview">
<audio id="en-audio" controls></audio> <audio id="en-audio" controls></audio>
</div> </div>
@@ -605,8 +605,8 @@
<!-- Batch Generation View --> <!-- Batch Generation View -->
<div id="view-batch" style="display: none; flex-direction: column; gap: 20px;"> <div id="view-batch" style="display: none; flex-direction: column; gap: 20px;">
<div class="form-group" id="batch-input-group"> <div class="form-group" id="batch-input-group">
<label>Paste Data (Format: ID &lt;Tab&gt; Chinese &lt;Tab&gt; English)</label> <label>Paste Data (Format: ID &lt;Tab&gt; Chinese &lt;Tab&gt; Japanese)</label>
<textarea id="batch-input" rows="8" placeholder="01-01&#9;千叶&#9;Chiba&#10;CK-01&#9;沼津&#9;Numazu" style="resize: vertical; font-family: var(--font-mono); white-space: pre;"></textarea> <textarea id="batch-input" rows="8" placeholder="01-01&#9;千叶&#9;ちば&#10;CK-01&#9;沼津&#9;ぬまづ" style="resize: vertical; font-family: var(--font-mono); white-space: pre;"></textarea>
<button class="btn btn-primary" id="btn-batch-preview" onclick="previewBatch()" style="margin-top: 10px;"> <button class="btn btn-primary" id="btn-batch-preview" onclick="previewBatch()" style="margin-top: 10px;">
<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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg> <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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Preview Tasks Preview Tasks
@@ -721,10 +721,10 @@
<script> <script>
const TTS_MODEL = "s2.1-pro-free"; const TTS_MODEL = "s2.1-pro-free";
const ZH_REFERENCE_ID = "0aee9f0f5683480d987c9e3c5d2de04c"; const ZH_REFERENCE_ID = "0aee9f0f5683480d987c9e3c5d2de04c";
const EN_REFERENCE_ID = "38985e545cff41b79c11c32b0196c9a0"; const JA_REFERENCE_ID = "38985e545cff41b79c11c32b0196c9a0";
let zhTempFile = null; let zhTempFile = null;
let enTempFile = null; let jaTempFile = null;
let listData = []; let listData = [];
let availableFiles = []; let availableFiles = [];
@@ -829,11 +829,11 @@
return data; return data;
} }
async function _importSingleAudio(code, zhTemp, enTemp) { async function _importSingleAudio(code, zhTemp, jaTemp) {
const res = await fetch('/api/tts/import', { const res = await fetch('/api/tts/import', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ stationCode: code, zhTemp, enTemp }) body: JSON.stringify({ stationCode: code, zhTemp, jaTemp })
}); });
const data = await res.json(); const data = await res.json();
if (!data.success) throw new Error(data.message); if (!data.success) throw new Error(data.message);
@@ -878,9 +878,9 @@
batchTasks.forEach((task, index) => { batchTasks.forEach((task, index) => {
// Initialize temp vars // Initialize temp vars
task.zhTempName = null; task.zhTempName = null;
task.enTempName = null; task.jaTempName = null;
task.zhUrl = null; task.zhUrl = null;
task.enUrl = null; task.jaUrl = null;
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'edit-file-item'; item.className = 'edit-file-item';
@@ -892,7 +892,7 @@
<div style="display:flex; justify-content:space-between; align-items:flex-start;"> <div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div style="display:flex; flex-direction:column; gap:2px;"> <div style="display:flex; flex-direction:column; gap:2px;">
<span style="font-weight:600; color:var(--accent)">${task.code}</span> <span style="font-weight:600; color:var(--accent)">${task.code}</span>
<span class="edit-file-name" style="color:var(--text-muted)">ZH: ${task.zh} | EN: ${task.en}</span> <span class="edit-file-name" style="color:var(--text-muted)">ZH: ${task.zh} | JA: ${task.en}</span>
</div> </div>
<span id="batch-status-span-${index}" style="font-size:0.8rem; color:var(--text-muted)">Pending</span> <span id="batch-status-span-${index}" style="font-size:0.8rem; color:var(--text-muted)">Pending</span>
</div> </div>
@@ -902,8 +902,8 @@
<audio id="batch-audio-zh-${index}" controls style="height:24px;"></audio> <audio id="batch-audio-zh-${index}" controls style="height:24px;"></audio>
</div> </div>
<div style="display:flex; align-items:center; gap:8px;"> <div style="display:flex; align-items:center; gap:8px;">
<span style="font-size:0.75rem; color:var(--text-muted); width:20px;">EN</span> <span style="font-size:0.75rem; color:var(--text-muted); width:20px;">JA</span>
<audio id="batch-audio-en-${index}" controls style="height:24px;"></audio> <audio id="batch-audio-ja-${index}" controls style="height:24px;"></audio>
</div> </div>
</div> </div>
`; `;
@@ -957,14 +957,14 @@
while (retryCount < 2 && !success) { while (retryCount < 2 && !success) {
try { try {
// 1. Generate ZH // 1. Generate ZH
const zhData = await _generateSingleTTS(task.zh, ZH_MODEL); const zhData = await _generateSingleTTS(task.zh, ZH_REFERENCE_ID);
// 2. Generate EN // 2. Generate JA
const enData = await _generateSingleTTS(task.en, EN_MODEL); const jaData = await _generateSingleTTS(task.en, JA_REFERENCE_ID);
task.zhTempName = zhData.tempName; task.zhTempName = zhData.tempName;
task.enTempName = enData.tempName; task.jaTempName = jaData.jaTempName || jaData.enTempName || jaData.tempName;
task.zhUrl = zhData.url; task.zhUrl = zhData.url;
task.enUrl = enData.url; task.jaUrl = jaData.jaUrl || jaData.enUrl || jaData.url;
success = true; success = true;
} catch (e) { } catch (e) {
@@ -987,7 +987,7 @@
const previewDiv = document.getElementById(`batch-audio-preview-${i}`); const previewDiv = document.getElementById(`batch-audio-preview-${i}`);
previewDiv.style.display = 'flex'; previewDiv.style.display = 'flex';
document.getElementById(`batch-audio-zh-${i}`).src = task.zhUrl; document.getElementById(`batch-audio-zh-${i}`).src = task.zhUrl;
document.getElementById(`batch-audio-en-${i}`).src = task.enUrl; document.getElementById(`batch-audio-ja-${i}`).src = task.jaUrl;
successCount++; successCount++;
} else { } else {
@@ -1031,7 +1031,7 @@
for (let i = 0; i < batchTasks.length; i++) { for (let i = 0; i < batchTasks.length; i++) {
const task = batchTasks[i]; const task = batchTasks[i];
if (!task.zhTempName || !task.enTempName) continue; // Skip failed ones if (!task.zhTempName || !task.jaTempName) continue; // Skip failed ones
// Replace slashes with dashes to ensure filesystem compatibility // Replace slashes with dashes to ensure filesystem compatibility
if (task.code.includes('/')) { if (task.code.includes('/')) {
@@ -1044,10 +1044,10 @@
statusSpan.style.color = 'var(--warning)'; statusSpan.style.color = 'var(--warning)';
try { try {
await _importSingleAudio(task.code, task.zhTempName, task.enTempName); await _importSingleAudio(task.code, task.zhTempName, task.jaTempName);
// Update listData // Update listData
const filesStr = `ch-${task.code}.wav,en-${task.code}.wav`; const filesStr = `ch-${task.code}.wav,ja-${task.code}.wav`;
const existingIndex = listData.findIndex(item => item.code === task.code); const existingIndex = listData.findIndex(item => item.code === task.code);
if (existingIndex >= 0) { if (existingIndex >= 0) {
listData[existingIndex].files = filesStr.split(','); listData[existingIndex].files = filesStr.split(',');
@@ -1088,10 +1088,10 @@
async function generateAudio() { async function generateAudio() {
const zhText = document.getElementById('zh-text').value.trim(); const zhText = document.getElementById('zh-text').value.trim();
const enText = document.getElementById('en-text').value.trim(); const jaText = document.getElementById('en-text').value.trim();
if (!zhText || !enText) { if (!zhText || !jaText) {
showToast("Error", "Please enter both Chinese and English names"); showToast("Error", "Please enter both Chinese and Japanese names");
return; return;
} }
@@ -1119,30 +1119,30 @@
const dataZh = await resZh.json(); const dataZh = await resZh.json();
if (!dataZh.success) throw new Error("ZH TTS failed: " + dataZh.message); if (!dataZh.success) throw new Error("ZH TTS failed: " + dataZh.message);
status.textContent = "Generating English audio..."; status.textContent = "Generating Japanese audio...";
// Generate English via Server-Side Proxy // Generate Japanese via Server-Side Proxy
const resEn = await fetch('/api/tts/generate', { const resJa = await fetch('/api/tts/generate', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
text: enText, text: jaText,
reference_id: EN_REFERENCE_ID, reference_id: JA_REFERENCE_ID,
model: TTS_MODEL model: TTS_MODEL
}) })
}); });
const dataEn = await resEn.json(); const dataJa = await resJa.json();
if (!dataEn.success) throw new Error("EN TTS failed: " + dataEn.message); if (!dataJa.success) throw new Error("JA TTS failed: " + dataJa.message);
// Success // Success
zhTempFile = dataZh.tempName; zhTempFile = dataZh.tempName;
enTempFile = dataEn.tempName; jaTempFile = dataJa.jaTempName || dataJa.enTempName || dataJa.tempName;
document.getElementById('zh-audio').src = dataZh.url; document.getElementById('zh-audio').src = dataZh.url;
document.getElementById('en-audio').src = dataEn.url; document.getElementById('en-audio').src = dataJa.jaUrl || dataJa.enUrl || dataJa.url;
document.getElementById('preview-section').style.display = 'block'; document.getElementById('preview-section').style.display = 'block';
status.textContent = "Generation complete!"; status.textContent = "Generation complete!";
@@ -1170,7 +1170,7 @@
code = code.replace(/\//g, '-'); code = code.replace(/\//g, '-');
} }
if (!zhTempFile || !enTempFile) { if (!zhTempFile || !jaTempFile) {
showToast("Error", "No audio generated to import"); showToast("Error", "No audio generated to import");
return; return;
} }
@@ -1186,7 +1186,7 @@
body: JSON.stringify({ body: JSON.stringify({
stationCode: code, stationCode: code,
zhTemp: zhTempFile, zhTemp: zhTempFile,
enTemp: enTempFile jaTemp: jaTempFile
}) })
}); });
@@ -1196,7 +1196,7 @@
showToast("Success", "Files imported to Station folder!"); showToast("Success", "Files imported to Station folder!");
// Automatically add/update to listData if generated via this panel // Automatically add/update to listData if generated via this panel
const filesStr = `ch-${code}.wav,en-${code}.wav`; const filesStr = `ch-${code}.wav,ja-${code}.wav`;
const existingIndex = listData.findIndex(item => item.code === code); const existingIndex = listData.findIndex(item => item.code === code);
if (existingIndex >= 0) { if (existingIndex >= 0) {
listData[existingIndex].files = filesStr.split(','); listData[existingIndex].files = filesStr.split(',');
@@ -1209,7 +1209,7 @@
// Reset // Reset
zhTempFile = null; zhTempFile = null;
enTempFile = null; jaTempFile = null;
document.getElementById('preview-section').style.display = 'none'; document.getElementById('preview-section').style.display = 'none';
document.getElementById('gen-status').textContent = ""; document.getElementById('gen-status').textContent = "";
document.getElementById('zh-text').value = ""; document.getElementById('zh-text').value = "";
+18 -13
View File
@@ -18,7 +18,7 @@ const wss = new WebSocket.Server({ server });
const PORT = Number(process.env.PORT || '8999'); const PORT = Number(process.env.PORT || '8999');
const DEFAULT_TTS_MODEL = process.env.TTS_MODEL || 's2.1-pro-free'; const DEFAULT_TTS_MODEL = process.env.TTS_MODEL || 's2.1-pro-free';
const DEFAULT_ZH_REFERENCE_ID = process.env.ZH_REFERENCE_ID || '0aee9f0f5683480d987c9e3c5d2de04c'; const DEFAULT_ZH_REFERENCE_ID = process.env.ZH_REFERENCE_ID || '0aee9f0f5683480d987c9e3c5d2de04c';
const DEFAULT_EN_REFERENCE_ID = process.env.EN_REFERENCE_ID || '38985e545cff41b79c11c32b0196c9a0'; const DEFAULT_JA_REFERENCE_ID = process.env.JA_REFERENCE_ID || process.env.EN_REFERENCE_ID || '38985e545cff41b79c11c32b0196c9a0';
const HLS_ENABLE = process.env.HLS_ENABLE !== '0'; const HLS_ENABLE = process.env.HLS_ENABLE !== '0';
const HLS_STREAM_MODE = (process.env.HLS_STREAM_MODE || 'event').toLowerCase(); const HLS_STREAM_MODE = (process.env.HLS_STREAM_MODE || 'event').toLowerCase();
@@ -2375,20 +2375,23 @@ app.post('/api/tts/upload_temp', upload.fields([{ name: 'zhAudio', maxCount: 1 }
// Rename to secure temp names // Rename to secure temp names
const zhTempName = `temp_zh_${Date.now()}_${Math.floor(Math.random() * 1000)}.wav`; const zhTempName = `temp_zh_${Date.now()}_${Math.floor(Math.random() * 1000)}.wav`;
const enTempName = `temp_en_${Date.now()}_${Math.floor(Math.random() * 1000)}.wav`; const jaTempName = `temp_ja_${Date.now()}_${Math.floor(Math.random() * 1000)}.wav`;
const zhTempPath = path.join(uploadDir, zhTempName); const zhTempPath = path.join(uploadDir, zhTempName);
const enTempPath = path.join(uploadDir, enTempName); const jaTempPath = path.join(uploadDir, jaTempName);
fs.renameSync(zhFile.path, zhTempPath); fs.renameSync(zhFile.path, zhTempPath);
fs.renameSync(enFile.path, enTempPath); fs.renameSync(enFile.path, jaTempPath);
res.json({ res.json({
success: true, success: true,
zhTempName, zhTempName,
enTempName, jaTempName,
zhUrl: `/uploads/${zhTempName}`, zhUrl: `/uploads/${zhTempName}`,
enUrl: `/uploads/${enTempName}` jaUrl: `/uploads/${jaTempName}`,
// Keep old keys temporarily so cached frontends do not break.
enTempName: jaTempName,
enUrl: `/uploads/${jaTempName}`
}); });
} catch (e) { } catch (e) {
res.status(500).json({ success: false, message: e.message }); res.status(500).json({ success: false, message: e.message });
@@ -2397,23 +2400,25 @@ app.post('/api/tts/upload_temp', upload.fields([{ name: 'zhAudio', maxCount: 1 }
// Import TTS temp files to station folder // Import TTS temp files to station folder
app.post('/api/tts/import', express.json(), (req, res) => { app.post('/api/tts/import', express.json(), (req, res) => {
const { stationCode, zhTemp, enTemp } = req.body; const stationCode = String(req.body?.stationCode || '').trim();
if (!stationCode || !zhTemp || !enTemp) return res.status(400).json({ success: false, message: 'Missing parameters' }); 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' });
const zhSource = path.join(uploadDir, zhTemp); const zhSource = path.join(uploadDir, zhTemp);
const enSource = path.join(uploadDir, enTemp); const jaSource = path.join(uploadDir, jaTemp);
const safeStationCode = String(stationCode).replace(/\//g, '-'); const safeStationCode = String(stationCode).replace(/\//g, '-');
const zhDest = path.join(audioStationDir, `ch-${safeStationCode}.wav`); const zhDest = path.join(audioStationDir, `ch-${safeStationCode}.wav`);
const enDest = path.join(audioStationDir, `en-${safeStationCode}.wav`); const jaDest = path.join(audioStationDir, `ja-${safeStationCode}.wav`);
try { try {
if (fs.existsSync(zhSource)) { if (fs.existsSync(zhSource)) {
fs.copyFileSync(zhSource, zhDest); fs.copyFileSync(zhSource, zhDest);
fs.unlinkSync(zhSource); fs.unlinkSync(zhSource);
} }
if (fs.existsSync(enSource)) { if (fs.existsSync(jaSource)) {
fs.copyFileSync(enSource, enDest); fs.copyFileSync(jaSource, jaDest);
fs.unlinkSync(enSource); fs.unlinkSync(jaSource);
} }
log('SUCCESS', `Imported generated audio for station ${stationCode}`); log('SUCCESS', `Imported generated audio for station ${stationCode}`);
res.json({ success: true }); res.json({ success: true });