Skip to content

Latest commit

 

History

History
296 lines (238 loc) · 11 KB

File metadata and controls

296 lines (238 loc) · 11 KB

这个插件在点击右键时,能够调用我的插件,然后插件能够读取当前选中的内容,调用azure的语音服务,将内容转换成语音文件。

const vscode = require('vscode');
const axios = require('axios');
const fs = require('fs');
const path = require('path');

let voiceList = [];
const voiceListPath = path.join(__dirname, 'voice-list.json');

// Load voice-list.json file
function loadVoiceList() {
    try {
        const data = fs.readFileSync(voiceListPath, 'utf-8');
        voiceList = JSON.parse(data);
        console.log("Voice list loaded successfully");
    } catch (error) {
        console.error("Error loading voice list: ", error);
    }
}

loadVoiceList();

function getVoiceAttributes() {
    let config = vscode.workspace.getConfiguration('speechify');
    return {
        name: config.get('voiceName'),
        gender: config.get('voiceGender'),
        style: config.get('voiceStyle')
    };
}

// Function to create quick pick items for dropdown lists
function createQuickPickItems(attribute, list, defaultValue) {
    const seen = new Set();
    const filteredList = list.filter(item => {
        if (seen.has(item[attribute])) {
            return false;
        }
        seen.add(item[attribute]);
        return true;
    });

    const items = filteredList.map(item => ({
        label: item[attribute],
        description: attribute === 'ShortName' || attribute === 'Style' ? item.LocalName : ''
    }));

    // Move defaultValue to the top if it exists
    if (defaultValue) {
        const defaultIndex = items.findIndex(item => item.label === defaultValue);
        if (defaultIndex !== -1) {
            const [defaultItem] = items.splice(defaultIndex, 1);
            items.unshift(defaultItem);
        }
    }

    return items;
}

// Function to show dropdowns and get the user selections
async function configureSpeechifyVoiceSettings() {
    const config = getVoiceAttributes();
    const defaultVoice = voiceList.find(voice => voice.ShortName === config.name);

    if (!defaultVoice) {
        vscode.window.showErrorMessage('Default voice not found in the voice list.');
        return;
    }

    console.log('defaultVoice:', defaultVoice);
    console.log('defaultVoice.LocaleName:', defaultVoice.LocaleName);

    // Select LocaleName
    let localeNamePick;
    const localeNameItems = createQuickPickItems('LocaleName', voiceList, defaultVoice.LocaleName);
    if (localeNameItems.length === 1) {
        localeNamePick = localeNameItems[0];
    } else if (localeNameItems.length > 1) {
        localeNamePick = await vscode.window.showQuickPick(localeNameItems, {
            placeHolder: 'Select Locale Name'
        });
    }

    if (!localeNamePick) return;

    const selectedVoicesByLocale = voiceList.filter(voice => voice.LocaleName === localeNamePick.label);

    // Select Gender
    let genderPick;
    const genderItems = createQuickPickItems('Gender', selectedVoicesByLocale, defaultVoice.Gender);
    if (genderItems.length === 1) {
        genderPick = genderItems[0];
    } else if (genderItems.length > 1) {
        genderPick = await vscode.window.showQuickPick(genderItems, {
            placeHolder: 'Select Gender'
        });
    }

    if (!genderPick) return;

    const selectedVoicesByGender = selectedVoicesByLocale.filter(voice => voice.Gender === genderPick.label);

    // Select ShortName
    let shortNamePick;
    const shortNameItems = createQuickPickItems('ShortName', selectedVoicesByGender, defaultVoice.ShortName);
    if (shortNameItems.length === 1) {
        shortNamePick = shortNameItems[0];
    } else if (shortNameItems.length > 1) {
        shortNamePick = await vscode.window.showQuickPick(shortNameItems, {
            placeHolder: 'Select Voice'
        });
    }

    if (!shortNamePick) return;

    // Find the selected voice to get its StyleList
    const selectedVoice = selectedVoicesByGender.find(voice => voice.ShortName === shortNamePick.label);
    if (!selectedVoice) return;

    // Select Style
    let stylePick;
    const styleItems = selectedVoice.StyleList?.map(style => ({
        label: style,
        description: ''
    })) || [];

    if (styleItems.length === 1) {
        stylePick = styleItems[0];
    } else if (styleItems.length > 1) {
        // Move default style to the top if it exists
        const defaultStyleIndex = styleItems.findIndex(item => item.label === config.style);
        if (defaultStyleIndex !== -1) {
            const [defaultStyleItem] = styleItems.splice(defaultStyleIndex, 1);
            styleItems.unshift(defaultStyleItem);
        }

        stylePick = await vscode.window.showQuickPick(styleItems, {
            placeHolder: 'Select Voice Style'
        });
    }

    // Use empty string if no style is selected
    const styleValue = stylePick ? stylePick.label : '';

    // Save the selections to configuration
    const updateConfig = vscode.workspace.getConfiguration('speechify');
    await updateConfig.update('voiceName', shortNamePick.label, vscode.ConfigurationTarget.Global);
    await updateConfig.update('voiceGender', genderPick.label, vscode.ConfigurationTarget.Global);
    await updateConfig.update('voiceStyle', styleValue, vscode.ConfigurationTarget.Global);
    showVoiceConfig(shortNamePick.label);
}

function showVoiceConfig(shortName) {
    const selectedVoice = voiceList.find(voice => voice.ShortName === shortName);

    if (selectedVoice) {
        const message = `
        Voice: ${selectedVoice.DisplayName}, Locale: ${selectedVoice.LocaleName}, ${selectedVoice.Gender}, Sample Rate: ${selectedVoice.SampleRateHertz} Hz, Words Per Minute: ${selectedVoice.WordsPerMinute}, Status: ${selectedVoice.Status}, Short Name: ${selectedVoice.ShortName}`;
        vscode.window.showInformationMessage(message);
    }
}


let config = vscode.workspace.getConfiguration('speechify');
let subscriptionKey = config.get('azureSpeechServicesKey');
let region = config.get('speechServicesRegion');

const endpoint = `https://${region}.tts.speech.microsoft.com`;


// Function to request speech from AzureTTS
function getSpeechFromAzureTTS(text, language, currentFile) {
    const currentDir = path.dirname(currentFile);
    const currentFileName = path.basename(currentFile, path.extname(currentFile));

    const date = new Date();
    const year = date.getFullYear();
    const month = ("0" + (date.getMonth() + 1)).slice(-2);
    const day = ("0" + date.getDate()).slice(-2);
    const currentDate = `${year}${month}${day}`;

    const newFileName = `${currentFileName}_${currentDate}.mp3`;
    const filePath = path.join(currentDir, newFileName);
    const voiceAttributes = getVoiceAttributes(language);

    showVoiceConfig(voiceAttributes.name);

    const ssml = `<speak version='1.0' xml:lang='${language}'>
                    <voice xml:lang='${language}' xml:gender='${voiceAttributes.gender}' name='${voiceAttributes.name}' style='${voiceAttributes.style}'>
                        ${text}
                    </voice>
                </speak>`;

    const url = `${endpoint}/cognitiveservices/v1`;

    const headers = {
        "Content-Type": "application/ssml+xml",
        "X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3",
        "Ocp-Apim-Subscription-Key": subscriptionKey
    };

    axios.post(url, ssml, { headers, responseType: "arraybuffer" }).then(response => {
        const audioData = Buffer.from(response.data, 'binary');
        let statusBarMessage = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
        fs.writeFile(filePath, audioData, function (err) {
            if (err) {
                console.error(err);
                vscode.window.showErrorMessage(`Error occurred while saving audio! ❌ Error: ${err.message}`);
            } else {
                console.log(`Audio saved as ${filePath}`);
                statusBarMessage.text = `✨ Speech audio saved successfully ✔️`;
                statusBarMessage.show();

                // Hide the status bar message after 10 seconds
                setTimeout(() => {
                    statusBarMessage.hide();
                }, 10000);  // 10000 milliseconds = 10 seconds
            }
        });
    }).catch(error => {
        console.error(error);
    });
}

// Function to configure Azure settings
async function configureSpeechifyAzureSettings() {
    const config = vscode.workspace.getConfiguration('speechify');

    const azureKey = config.get('azureSpeechServicesKey');
    const azureRegion = config.get('speechServicesRegion');

    // Input box for Azure Speech Services Key
    const newAzureKey = await vscode.window.showInputBox({
        value: azureKey,
        placeHolder: 'Enter Azure Speech Services Key',
        prompt: 'Azure Speech Services Key'
    });

    if (newAzureKey === undefined) return; // User cancelled input

    // Input box for Azure Speech Services Region
    const newAzureRegion = await vscode.window.showInputBox({
        value: azureRegion,
        placeHolder: 'Enter Azure Speech Services Region',
        prompt: 'Azure Speech Services Region'
    });

    if (newAzureRegion === undefined) return; // User cancelled input

    // Save the new settings
    await config.update('azureSpeechServicesKey', newAzureKey, vscode.ConfigurationTarget.Global);
    await config.update('speechServicesRegion', newAzureRegion, vscode.ConfigurationTarget.Global);

    vscode.window.showInformationMessage('Azure Speech Services settings have been updated.');
}

// Activation of the extension
function activate(context) {
    let disposable = vscode.commands.registerCommand('extension.speechify', function () {
        const editor = vscode.window.activeTextEditor;
        if (!editor) {
            return; // No open text editor
        }

        const selection = editor.selection;
        const text = editor.document.getText(selection);
        const currentFile = editor.document.fileName;
        const language = getVoiceAttributes().name.split('-').slice(0, 2).join('-'); // assume that the language is the prefix of the voice name

        getSpeechFromAzureTTS(text, language, currentFile);
    });

    // Register the configure voice settings command
    let configureVoiceDisposable = vscode.commands.registerCommand('extension.configureSpeechifyVoiceSettings', configureSpeechifyVoiceSettings);
    // Register the configure Azure settings command
    let configureAzureDisposable = vscode.commands.registerCommand('extension.configureSpeechifyAzureSettings', configureSpeechifyAzureSettings);

    context.subscriptions.push(disposable);
    context.subscriptions.push(configureAzureDisposable);
    context.subscriptions.push(configureVoiceDisposable);
}

// Deactivation of the extension
function deactivate() { }

module.exports = {
    activate,
    deactivate
}


我希望在生成音频时,改为使用vscode.window.showInformationMessage来显示音频生成成功的消息,这样用户就能够更直观的看到音频生成成功的消息。
另外,在生成SSML时,要考虑到用户输入的文本中可能包含特殊字符,需要对这些特殊字符进行转义,以避免SSML解析错误。