128 lines
3.6 KiB
JavaScript
128 lines
3.6 KiB
JavaScript
import { mkdirSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const outputDir = resolve('src/assets/audio/voice/prologue');
|
|
|
|
mkdirSync(outputDir, { recursive: true });
|
|
|
|
const voiceName = 'Microsoft Heami Desktop';
|
|
|
|
const lines = [
|
|
{
|
|
name: 'yellow-turban-chaos',
|
|
role: 'narrator',
|
|
rate: -2,
|
|
volume: 86,
|
|
text: '중평 원년, 황건의 깃발이 각지에서 일어섰다. 관군은 흩어지고, 마을마다 불안한 소문이 번져갔다.'
|
|
},
|
|
{
|
|
name: 'liu-bei-resolve',
|
|
role: 'liuBei',
|
|
rate: -1,
|
|
volume: 92,
|
|
text: '천실의 백성이 이토록 고통받는데, 어찌 초개처럼 그늘에 숨어 있을 수 있겠는가.'
|
|
},
|
|
{
|
|
name: 'zhang-fei-joins',
|
|
role: 'zhangFei',
|
|
rate: 1,
|
|
volume: 100,
|
|
text: '좋소! 나라를 바로잡고 백성을 구한다면, 내 재산과 힘을 모두 아끼지 않겠소.'
|
|
},
|
|
{
|
|
name: 'guan-yu-joins',
|
|
role: 'guanYu',
|
|
rate: -3,
|
|
volume: 95,
|
|
text: '서로의 뜻을 믿고 의로운 길을 함께한다면, 천 리라도 멀다 하지 않겠습니다.'
|
|
},
|
|
{
|
|
name: 'oath-narration',
|
|
role: 'narrator',
|
|
rate: -2,
|
|
volume: 88,
|
|
text: '복사꽃 향기가 흩날리는 동산에서 세 사람은 하늘과 땅 앞에 고했다.'
|
|
},
|
|
{
|
|
name: 'oath-liu-bei',
|
|
role: 'liuBei',
|
|
rate: -1,
|
|
volume: 94,
|
|
text: '우리는 태어난 날은 달라도, 오늘부터 뜻을 함께한다. 백성을 구하고 천하를 바로 세우는 길에서 물러서지 않겠다.'
|
|
},
|
|
{
|
|
name: 'oath-brothers',
|
|
role: 'guanYu',
|
|
rate: -3,
|
|
volume: 96,
|
|
text: '의리를 저버리지 않고, 형제를 저버리지 않겠습니다.'
|
|
},
|
|
{
|
|
name: 'militia-rises',
|
|
role: 'zhangFei',
|
|
rate: 1,
|
|
volume: 100,
|
|
text: '창을 들 사람은 앞으로 나오시오! 오늘 모인 이름 없는 무리가 내일의 방패가 될 것이오!'
|
|
},
|
|
{
|
|
name: 'first-sortie',
|
|
role: 'liuBei',
|
|
rate: -1,
|
|
volume: 94,
|
|
text: '우리는 아직 작다. 그러나 오늘 한 마을을 지켜낸다면, 그 뜻은 천하로 번져갈 것이다.'
|
|
},
|
|
{
|
|
name: 'battle-briefing',
|
|
role: 'narrator',
|
|
rate: -2,
|
|
volume: 88,
|
|
text: '의용군은 마을 입구로 향했다. 첫 전투가 곧 시작된다.'
|
|
}
|
|
];
|
|
|
|
for (const line of lines) {
|
|
const output = resolve(outputDir, `${line.name}.wav`);
|
|
generateVoice({ ...line, output });
|
|
}
|
|
|
|
function generateVoice(item) {
|
|
const payload = Buffer.from(JSON.stringify({ ...item, voiceName }), 'utf8').toString('base64');
|
|
const powershell = `
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
Add-Type -AssemblyName System.Speech
|
|
$payload = '${payload}'
|
|
$json = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload))
|
|
$item = $json | ConvertFrom-Json
|
|
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
try {
|
|
try {
|
|
$synth.SelectVoice($item.voiceName)
|
|
} catch {
|
|
$installed = $synth.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }
|
|
if ($installed.Count -gt 0) {
|
|
$synth.SelectVoice($installed[0])
|
|
}
|
|
}
|
|
$synth.Rate = [int]$item.rate
|
|
$synth.Volume = [int]$item.volume
|
|
$synth.SetOutputToWaveFile($item.output)
|
|
$synth.Speak($item.text)
|
|
} finally {
|
|
$synth.Dispose()
|
|
}
|
|
`;
|
|
|
|
const encodedCommand = Buffer.from(powershell, 'utf16le').toString('base64');
|
|
const result = spawnSync(
|
|
'powershell.exe',
|
|
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encodedCommand],
|
|
{ stdio: 'inherit' }
|
|
);
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`Failed to generate voice asset: ${item.name}`);
|
|
}
|
|
}
|