PWSH AI 查词、发音

演示如何使用 powershell 编写调用 AI 查询单词释义、调用 TTS 生成发音文件并进行播放的例子
可以定义 shell 环境, code $PROFILE 加入快捷调用函数,比如 wd word (脚本见最后)

单词查询脚本


<#
    .DESCRIPTION 
        查询单词
    .EXAMPLE
        .\SearchWork.ps1 -word "example"
    .NOTES
        - 单词语音合成使用 edge-tts 生成并播放,需提前安装 python 应用 edge-tts , 安装方法: pipx (pip) install edge-tts
        - edge-tts 可能在 ~/.local/bin/ 下, 需要将该路径添加到系统 PATH 中, 以便 powershell 脚本调用
        - 语音合成以可以使用本地 TTS 引擎, Piper TTS, 有空再研究
        - Windows 下使用 PresentationCore.dll 中的 MediaPlayer 类播放音频。 
        - linux 下使用 mpg123 播放音频, 需要提前安装 mpg123, 安装方法: sudo apt install mpg123
        - 单词释义使用 code365scripts.openai 模块调用 AI 接口,配置了一个 ty 的 profile, 见: https://blog.xsoft.ltd/2025/10/13/1456/
        - 单词释义也可以调用接口 https://api.dictionaryapi.dev/api/v2/entries/en/$($this.word) 获取,以后根据需要再研究
        - 需要安装 glow 用于渲染 markdown, 安装方法: winget install glow /   https://github.com/charmbracelet/glow
#>

param (
    [string]$word = "example"
)

$ErrorActionPreference = "Stop"

Import-Module  code365scripts.openai -Force

# 后台工作的辅助类
class ThreadRunner : System.IDisposable {

    ThreadRunner([runspace]$runspace, [powershell]$ps, [System.IAsyncResult] $asyncResult) {
        $this._runspace = $runspace
        $this._ps = $ps        
        $this._asyncResult = $asyncResult
    }

    [runspace] $_runspace
    [powershell] $_ps
    [System.IAsyncResult] $_asyncResult

    [void] wait() {

        $this._ps.EndInvoke($this._asyncResult)
        $this.Dispose()   

    }

    static [ThreadRunner]run([scriptblock]$scriptBlock, [object[]]$arguments) {

        $runspace = [runspacefactory]::CreateRunspace()
        $runspace.ApartmentState = "STA"
        $runspace.Open()

        $ps = [powershell]::Create()
        $ps.Runspace = $runspace
        $ps.AddScript($scriptBlock)

        if ($arguments) {
            foreach ( $arg in $arguments ) {
                $ps.AddArgument($arg)
            }
        }

        $asyncResult = $ps.BeginInvoke()

        return [ThreadRunner]::new($runspace, $ps, $asyncResult)
    }

    [void] Dispose() {

        $this._ps.Dispose()
        $this._runspace.Close()
        $this._runspace.Dispose()
    }
}

# 单词发音辅助类
class Pronunciation : System.IDisposable {

    Pronunciation() {
        $name = [System.IO.Path]::GetRandomFileName() + ".mp3"
        $tmpDir = [System.IO.Path]::GetTempPath()
        $this.voiceFile = Join-Path -Path $tmpDir -ChildPath $name
    }

    # 存放发音文件路径
    [string] $voiceFile

    static [bool] isWindows() {
        return $global:IsWindows
    }

    # 播放音频, 仅在 windows 下可用
    static [void] playVoice([string] $file) {

        if ( -not (Test-Path -Path $file) ) {
            throw "发音文件不存在: $($file)"
        }

        Add-Type -AssemblyName PresentationCore

        $player = New-Object System.Windows.Media.MediaPlayer
        $player.Open($file)
        $player.Volume = 1  # 确保音量最大

        try {

            # 等待加载完成
            while ($player.NaturalDuration.HasTimeSpan -eq $false) {
                Start-Sleep -Milliseconds 100
            }

            # 播放
            $player.Play()

            # 等待播放结束(根据时长)
            $duration = $player.NaturalDuration.TimeSpan.TotalSeconds
            Start-Sleep -Seconds ([Math]::Ceiling($duration))            
        }
        finally {
            $player.Stop()
            $player.Close()
        }
    }

    [ThreadRunner] play() {

        # 因为后台无法直接调用 playVoice 方法,所以这里包装入 scriptblock 传递过去
        $palyVoiceScript = {
            param ($file)
            [Pronunciation]::playVoice($file)
        }

        if(-not [Pronunciation]::isWindows()){

            $palyVoiceScript = {
            param ($file)
                & mpg123 $file
            }        
        }

        $script = {

            param ($file, [scriptblock]$playVoice)

            if (-not (Test-Path -Path $file)) {
                throw "发音文件生成失败: $($file)"
            }

            # 播放5次
            for ($idx = 0; $idx -lt 5; $idx++) {

                & $playVoice $file
                Start-Sleep -Seconds 0.5
            }
        }

        $runner = [ThreadRunner]::run($script, @($this.voiceFile, $palyVoiceScript))

        return $runner

    }

    # 获取 edge-tts 可执行文件路径
    [string] getEdgeTtsPath() {

        if ([Pronunciation]::isWindows()) {
            # Windows 下直接使用 edge-tts 命令
            return 'edge-tts'
        }

        # ubuntu 下无法定位 edge-tts , 这里指定一下完整的路径
        $localBinPath = Join-Path -Path $env:HOME -ChildPath '.local' | join-path -childpath 'bin' | join-path -childpath 'edge-tts'    

        if (Test-Path -Path $localBinPath) {
            return $localBinPath
        }

        throw "edge-tts 未安装, $localBinPath。"
    }

    # 生成发音文件
    [ThreadRunner] pronounce([string] $word) {

        [string]$appPath = $this.getEdgeTtsPath();

        $script = {

            param ($word, $file, $appPath)

            # 生成发音文件, ai 推荐这几位的发音 en-US-JennyNeural | en-US-EmmaNeural | en-US-AriaNeural             
            & $appPath --text $word --voice "en-US-JennyNeural" --write-media $file

            if (-not (Test-Path -Path $file)) {
                throw "发音文件生成失败: $($file)"
            }
        }

        $runner = [ThreadRunner]::run($script, @($word, $this.voiceFile, $appPath))

        return $runner
    }

    [void] Dispose() {
        if (Test-Path -Path $this.voiceFile) {
            Remove-Item -Path $this.voiceFile -Force
        }
    }
}   

# 判断当前是否是 windows 系统
if ($IsWindows) {
    # 设置代码页为 UTF-8
    chcp 65001 | Out-Null
}
else {
    # 三方组件中使用了变量 USERPROFILE , 在非 windows 系统下该值为空, 需要设置为 HOME
    $env:USERPROFILE = $env:HOME
}

# 设置 PowerShell 输出编码为 UTF-8
$OutputEncoding = [System.Text.UTF8Encoding]::new()

$pronunciation = [Pronunciation]::new()

try {

    # 生成发音
    $runner = $pronunciation.pronounce($word)

    # 查询释义
    $markdown = New-ChatCompletions -context @{word = $word } -system '你是一个专业、权威的智能词典,擅长解释词语、术语、缩略语、成语和俚语。你能够:\n0. 给出清晰、简洁的定义及音标。\n1. 该单词背诵的技巧,可以是任意类型的技巧 n2. 指出词性、语法类别(如名词、动词、形容词等)。\n3. 提供一个或多个相关例句。\n4. 提供常见用法、词源、近义词或反义词(如有)。\n5. 支持中英文词语,自动识别语言。\n请保持结构清晰,并尽可能使用 Markdown 格式输出,便于阅读。尽量使用中文进行回复。' -prompt '请给出{{word}}的解释' -profile 'ty'

    # 生成语音文件
    $runner.wait()

    # 渲染 markdown 输出, &  新开进各程执行 glow
    $markdown | & glow

    # 播放发音
    $runner = $pronunciation.play()

    # 等待发音完成 
    $runner.wait()
}
finally {
    $pronunciation.Dispose()
}

调用脚本

'vim $PROFILE' 或 code $PROFILE.CurrentUser* code $PROFILE.AllUsers*


function wd {
Write-Host "[Debug] WordDict args: $args" -ForegroundColor Green

$path = '/opt/bin/search-word.ps1'
if($IsWindows){
    $path= 'C:\Users\icoms\bin\WordDict.ps1'
}

if (Test-Path $path) {

    & $path @args

} else {

    Write-Error "WordDict.ps1 not exist"

}

}

上一篇
下一篇