Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 变更日志(CHANGELOG)

## 2025-08-21
本次为次版本更新,增加局域网部署体验,新增应用内设置能力与多语言完善。

- [Added]
- 应用内“设置”页:支持读取/保存配置并“一键保存并重启”。
- 路由:`/settings`(`src/renderer/src/views/settings/index.vue`)
- 预加载桥接:`window.settings.get/save/restart`(`src/preload/index.js`)
- 主进程 IPC:`settings/get`、`settings/save`、`settings/restart`(`src/main/service/settings.js`)
- 持久化配置:从 `app.getPath('userData')/config.json` 读取/写入;优先级:config.json > 环境变量 > 默认值(`src/main/config/config.js`)。
- 顶部“设置”按钮整合“应用设置”入口,直达“设置”页(`src/renderer/src/components/AppHeader.vue`)。
- i18n 补齐:`common.menu.settings`、`common.setting.appSettingsText`(`src/renderer/src/i18n/components/common.js`)。

- [Changed]
- 默认 LAN 配置更新(Windows UNC 路径已正确转义):
- `HEYGEM_HOST=192.168.110.22`
- `HEYGEM_TTS_PORT=18180`,`HEYGEM_VIDEO_PORT=8383`
- `HEYGEM_MODEL_DIR=\\\\192.168.110.22\\heygem_data\\face2face\\temp`
- `HEYGEM_TTS_PRODUCT_DIR=\\\\192.168.110.22\\heygem_data\\face2face\\temp`
- `HEYGEM_TTS_ROOT=\\\\192.168.110.22\\heygem_data\\voice\\data`
- `HEYGEM_TTS_TRAIN_DIR=\\\\192.168.110.22\\heygem_data\\voice\\data\\origin_audio`
- 侧边栏菜单仅保留“首页”,移除“设置”入口(`src/renderer/src/components/menuLIst.vue`),避免重复入口。
- 菜单文案改为 `t(item.key)` 渲染,语言切换即时生效(`menuLIst.vue`)。

- [Fixed]
- 修复 UNC 默认路径字符串转义错误导致的语法/打包告警(`src/main/config/config.js`)。
- 多语言缺失键导致显示 key 文本的问题(`common.js`)。

- [Deployment Notes]
- 首次安装或升级后,建议进入“应用设置”保存参数并自动重启,以写入 `config.json`。
- 局域网部署请确保 SMB 共享与读写权限:`\\192.168.110.22\heygem_data\...`。
- 根据实际需求修改`192.168.110.22`为实际IP地址。

- [Upgrade Guide]
1) 卸载旧版或直接覆盖安装。
2) 打开应用 → 右上角“设置” → “应用设置” → 填写并保存 → 自动重启生效。
3) 多人环境:确认各客户端均能访问 SMB 共享与服务端口。
73 changes: 59 additions & 14 deletions src/main/config/config.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,70 @@
import path from 'path'
import os from 'os'
import fs from 'fs'
import { app } from 'electron'

const isDev = process.env.NODE_ENV === 'development'
const isWin = process.platform === 'win32'

function loadUserConfig() {
try {
// Electron user-specific config dir
const userDataDir = app && app.getPath ? app.getPath('userData') : null
let cfgPath
if (userDataDir) {
cfgPath = path.join(userDataDir, 'config.json')
} else {
// Fallback for non-Electron contexts
const fallbackBase = isWin
? path.join(os.homedir(), 'AppData', 'Roaming', 'HeyGem')
: path.join(os.homedir(), '.config', 'HeyGem')
cfgPath = path.join(fallbackBase, 'config.json')
}
if (fs.existsSync(cfgPath)) {
const raw = fs.readFileSync(cfgPath, 'utf-8')
return JSON.parse(raw)
}
} catch (e) {
// ignore parse or fs errors and fallback to env/defaults
}
return {}
}

const CFG = loadUserConfig()

// Allow overriding service host/ports via environment variables for LAN deployment
// HEYGEM_HOST: target host or IP (e.g., your LAN IP like 192.168.1.100)
// HEYGEM_TTS_PORT: TTS service host port (default 18180)
// HEYGEM_VIDEO_PORT: Face2Face service host port (default 8383)
const HEYGEM_HOST = CFG.HEYGEM_HOST || process.env.HEYGEM_HOST || '192.168.110.22'
const HEYGEM_TTS_PORT = CFG.HEYGEM_TTS_PORT || process.env.HEYGEM_TTS_PORT || '18180'
const HEYGEM_VIDEO_PORT = CFG.HEYGEM_VIDEO_PORT || process.env.HEYGEM_VIDEO_PORT || '8383'

export const serviceUrl = {
face2face: isDev ? 'http://192.168.4.204:8383/easy' : 'http://127.0.0.1:8383/easy',
tts: isDev ? 'http://192.168.4.204:18180' : 'http://127.0.0.1:18180'
face2face: `http://${HEYGEM_HOST}:${HEYGEM_VIDEO_PORT}/easy`,
tts: `http://${HEYGEM_HOST}:${HEYGEM_TTS_PORT}`
}

export const assetPath = {
model: isWin
? path.join('D:', 'heygem_data', 'face2face', 'temp')
: path.join(os.homedir(), 'heygem_data', 'face2face', 'temp'), // 模特视频
ttsProduct: isWin
? path.join('D:', 'heygem_data', 'face2face', 'temp')
: path.join(os.homedir(), 'heygem_data', 'face2face', 'temp'), // TTS 产物
ttsRoot: isWin
? path.join('D:', 'heygem_data', 'voice', 'data')
: path.join(os.homedir(), 'heygem_data', 'voice', 'data'), // TTS服务根目录
ttsTrain: isWin
? path.join('D:', 'heygem_data', 'voice', 'data', 'origin_audio')
: path.join(os.homedir(), 'heygem_data', 'voice', 'data', 'origin_audio') // TTS 训练产物
// Allow overriding asset directories to support LAN shared folders
model:
CFG.HEYGEM_MODEL_DIR || process.env.HEYGEM_MODEL_DIR ||
(isWin
? '\\\\192.168.110.22\\heygem_data\\face2face\\temp'
: path.join(os.homedir(), 'heygem_data', 'face2face', 'temp')), // 模特视频
ttsProduct:
CFG.HEYGEM_TTS_PRODUCT_DIR || process.env.HEYGEM_TTS_PRODUCT_DIR ||
(isWin
? '\\\\192.168.110.22\\heygem_data\\face2face\\temp'
: path.join(os.homedir(), 'heygem_data', 'face2face', 'temp')), // TTS 产物
ttsRoot:
CFG.HEYGEM_TTS_ROOT || process.env.HEYGEM_TTS_ROOT ||
(isWin
? '\\\\192.168.110.22\\heygem_data\\voice\\data'
: path.join(os.homedir(), 'heygem_data', 'voice', 'data')), // TTS服务根目录
ttsTrain:
CFG.HEYGEM_TTS_TRAIN_DIR || process.env.HEYGEM_TTS_TRAIN_DIR ||
(isWin
? '\\\\192.168.110.22\\heygem_data\\voice\\data\\origin_audio'
: path.join(os.homedir(), 'heygem_data', 'voice', 'data', 'origin_audio')) // TTS 训练产物
}
2 changes: 2 additions & 0 deletions src/main/service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { init as videoResult } from './video.js'
import { init as model } from './model.js'
import { init as context } from './context.js'
import { init as voice } from './voice.js'
import { init as settings } from './settings.js'
export function registerHandler() {
videoResult()
model()
context()
voice()
settings()
}
54 changes: 54 additions & 0 deletions src/main/service/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ipcMain, app } from 'electron'
import fs from 'fs'
import path from 'path'

const MODEL_NAME = 'settings'

function getConfigPath() {
const userDataDir = app.getPath('userData')
return path.join(userDataDir, 'config.json')
}

function ensureDirExists(filePath) {
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
}

function readConfig() {
const cfgPath = getConfigPath()
try {
if (fs.existsSync(cfgPath)) {
const raw = fs.readFileSync(cfgPath, 'utf-8')
return JSON.parse(raw)
}
} catch (e) {
// ignore and return empty
}
return {}
}

function writeConfig(cfg) {
const cfgPath = getConfigPath()
ensureDirExists(cfgPath)
fs.writeFileSync(cfgPath, JSON.stringify(cfg ?? {}, null, 2), 'utf-8')
return true
}

function restartApp() {
app.relaunch()
app.exit(0)
}

export function init() {
ipcMain.handle(MODEL_NAME + '/get', () => {
return readConfig()
})
ipcMain.handle(MODEL_NAME + '/save', (event, cfg) => {
return writeConfig(cfg)
})
ipcMain.handle(MODEL_NAME + '/restart', () => {
restartApp()
})
}
19 changes: 15 additions & 4 deletions src/main/service/voice.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,23 @@ export async function train(path, lang = 'zh') {
lang
})
log.debug('~ train ~ res:', res)
// Ensure response is valid
if (!res || typeof res !== 'object') {
throw new Error('TTS preprocess failed: empty response')
}
if (res.code !== 0) {
return false
} else {
const { asr_format_audio_url, reference_audio_text } = res
return insert({ origin_audio_path: path, lang, asr_format_audio_url, reference_audio_text })
const msg = res.message || res.msg || 'unknown error'
throw new Error(`TTS preprocess failed: code=${res.code}, message=${msg}`)
}
const { asr_format_audio_url, reference_audio_text } = res
if (!asr_format_audio_url || !reference_audio_text) {
throw new Error('TTS preprocess failed: missing required fields asr_format_audio_url/reference_audio_text')
}
const voiceId = insert({ origin_audio_path: path, lang, asr_format_audio_url, reference_audio_text })
if (typeof voiceId !== 'number' && typeof voiceId !== 'bigint') {
throw new Error('Insert voice failed: invalid voiceId returned')
}
return voiceId
}

export function makeAudio4Video({voiceId, text}) {
Expand Down
7 changes: 7 additions & 0 deletions src/preload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { exposeWebHandles } from '../main/handlers/index'
// Custom APIs for renderer

const client = exposeWebHandles(electronAPI)
const settingsAPI = {
get: () => electronAPI.ipcRenderer.invoke('settings/get'),
save: (cfg) => electronAPI.ipcRenderer.invoke('settings/save', cfg),
restart: () => electronAPI.ipcRenderer.invoke('settings/restart')
}

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
Expand All @@ -13,10 +18,12 @@ if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('client', client)
contextBridge.exposeInMainWorld('settings', settingsAPI)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.client = client
window.settings = settingsAPI
}
13 changes: 13 additions & 0 deletions src/renderer/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ export function saveContext(key, val) {
export function audition(voiceId, text) {
return window.electron.ipcRenderer.invoke('voice/audition', voiceId, text)
}

// Settings wrappers
export function getSettings() {
return window.settings.get()
}

export function saveSettings(cfg) {
return window.settings.save(cfg)
}

export function restartApp() {
return window.settings.restart()
}
9 changes: 9 additions & 0 deletions src/renderer/src/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ import { useHomeStore } from '@renderer/stores/home.js'
import { useI18n } from 'vue-i18n'
import { saveContext } from '@renderer/api/index.js'
import { lang_ } from '@renderer/utils/const.js'
import { useRouter } from 'vue-router'
const { locale, t } = useI18n()
const home = useHomeStore()
const router = useRouter()
const state = reactive({
isMaximized: false,
menuList: [
Expand All @@ -70,6 +72,11 @@ const state = reactive({
key: 'common.setting.tab.openLogText',
value: 'openLog'
},
{
content: '应用设置',
key: 'common.setting.appSettingsText',
value: 'appSettings'
},
{
content: '语言切换',
value: 'languageSwitch',
Expand Down Expand Up @@ -117,6 +124,8 @@ const action = {
home.setAgreementVisible(true)
} else if (value === 'openLog') {
Client.app.openLog()
} else if (value === 'appSettings') {
router.push('/settings')
} else {
if (value === 'languageSwitch') return
window.localStorage.setItem('language', value)
Expand Down
15 changes: 4 additions & 11 deletions src/renderer/src/components/menuLIst.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="content-body">
<img class="icon" :src="item.active ? item.onIcon : item.offIcon" />
<div class="text" :style="item.active ? 'color: #434AF9' : ''">
{{ item.name }}
{{ t(item.key) }}
</div>
<img class="active-icon" v-if="item.active" :src="activeIcon" />
</div>
Expand All @@ -13,23 +13,22 @@
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { reactive, watch, computed } from 'vue'
import { reactive, watch } from 'vue'
import onIcon from '../assets/images/home/menu/onHome.svg'
import activeIcon from '../assets/images/home/menu/active.svg'
import offIcon from '../assets/images/home/menu/offHome.svg'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const { t } = useI18n()
const unRoute = useRoute()
const router = useRouter()
const obj = [
{
key: 'common.menu.text',
name: t('common.menu.text'),
onIcon,
offIcon,
active: true,
path: '/home'
}
},
/* {
name: "账号",
onIcon,
Expand All @@ -41,12 +40,6 @@ const obj = [
const state = reactive({
menuList: obj
})

watch(locale, () => {
state.menuList.forEach((el, index) => {
el.name = t(el.key)
})
})
watch(
() => unRoute.path,
(newPath) => {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/src/i18n/components/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Button } from 'tdesign-vue-next'
// 中文翻译
export const commonZh = {
menu: {
text: '首页'
text: '首页',
settings: '设置'
},
header: {
minimizeText: '最小化',
Expand Down Expand Up @@ -138,6 +139,7 @@ export const commonZh = {
languageSwitchText: '语言切换',
openLogText: '打开日志'
},
appSettingsText: '应用设置',
languageSwitch: {
languageEnText: '英文',
languageZhText: '中文'
Expand All @@ -148,7 +150,8 @@ export const commonZh = {
// 英文翻译
export const commonEn = {
menu: {
text: 'Home'
text: 'Home',
settings: 'Settings'
},
header: {
minimizeText: 'Minimize',
Expand Down Expand Up @@ -279,6 +282,7 @@ export const commonEn = {
languageSwitchText: 'Language switch',
openLogText: 'Open Log'
},
appSettingsText: 'App Settings',
languageSwitch: {
languageEnText: 'English',
languageZhText: 'Chinese'
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import home from '@renderer/views/home/index.vue'
import account from '@renderer/views/account/index.vue'
import VideoEditView from '@renderer/views/video-edit/VideoEditView.vue'
import SettingsView from '@renderer/views/settings/index.vue'

const router = createRouter({
history: createWebHashHistory(),
Expand All @@ -26,6 +27,11 @@ const router = createRouter({
name: 'account',
component: account
},
{
path: '/settings',
name: 'settings',
component: SettingsView
},
]
})

Expand Down
Loading