dsh-audiogen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +67 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +2457 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1345 -0
- package/package.json +93 -0
- package/skills/design/SKILL.md +13 -0
- package/skills/music/SKILL.md +16 -0
- package/skills/sfx/SKILL.md +16 -0
- package/skills/tts/SKILL.md +18 -0
- package/src/agent-audio-tools.ts +190 -0
- package/src/audio-engine.ts +377 -0
- package/src/audio-presets.ts +80 -0
- package/src/audio-store.ts +131 -0
- package/src/client/AudioGenPanel.tsx +206 -0
- package/src/client/SettingsCard.tsx +337 -0
- package/src/client/api.ts +36 -0
- package/src/client/audio-panel.module.css +198 -0
- package/src/client/audio-toolview.module.css +69 -0
- package/src/client/audio-toolview.tsx +119 -0
- package/src/client/channels-form.ts +263 -0
- package/src/client/controller.ts +44 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/helpers.ts +27 -0
- package/src/client/index.ts +103 -0
- package/src/client/locales.ts +133 -0
- package/src/client/mount.tsx +96 -0
- package/src/client/panel.module.css +1566 -0
- package/src/client/settings-card.module.css +1023 -0
- package/src/client/settings-form.ts +336 -0
- package/src/client/settings-scope.ts +289 -0
- package/src/client/sidebar-entry.ts +115 -0
- package/src/index.ts +201 -0
- package/src/protocol.ts +179 -0
- package/src/routes.ts +386 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared panel helpers: active-dictionary pick and a small error extractor.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { en, zh, type AudioGenKey } from './locales.ts'
|
|
6
|
+
|
|
7
|
+
export type TranslateValues = Record<string, string | number>
|
|
8
|
+
|
|
9
|
+
export function dictionary(): Record<string, string> {
|
|
10
|
+
const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
|
|
11
|
+
return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function tt(key: AudioGenKey, values?: TranslateValues): string {
|
|
15
|
+
const text = dictionary()[key] ?? key
|
|
16
|
+
if (values === undefined) return text
|
|
17
|
+
let rendered = text
|
|
18
|
+
for (const [name, value] of Object.entries(values)) {
|
|
19
|
+
rendered = rendered.replaceAll(`{${name}}`, String(value))
|
|
20
|
+
}
|
|
21
|
+
return rendered
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function errorMessage(error: unknown): string {
|
|
25
|
+
if (error instanceof Error) return error.message
|
|
26
|
+
return String(error)
|
|
27
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-half entry for dsh-audiogen.
|
|
3
|
+
*
|
|
4
|
+
* Registers locale dictionaries, the settings card (Settings → Plugins → AI
|
|
5
|
+
* 音频), and mounts the sidebar entry + generation panel. DOM mounting
|
|
6
|
+
* failures are logged, never thrown.
|
|
7
|
+
*/
|
|
8
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
9
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
10
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
12
|
+
import { AudiogenApi } from './api.ts'
|
|
13
|
+
import { AudioGenController } from './controller.ts'
|
|
14
|
+
import { tt } from './helpers.ts'
|
|
15
|
+
import { en, zh, type AudioGenKey } from './locales.ts'
|
|
16
|
+
import { mountPanel } from './mount.tsx'
|
|
17
|
+
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
18
|
+
import { AudioGenSettingsCard, AudioGenSettingsCardController } from './SettingsCard.tsx'
|
|
19
|
+
import { bindAudiogenScope, type AudiogenScope } from './settings-scope.ts'
|
|
20
|
+
import { registerAudioToolviews } from './audio-toolview.tsx'
|
|
21
|
+
|
|
22
|
+
const NS = 'dsh-audiogen'
|
|
23
|
+
|
|
24
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
25
|
+
interface LocaleNamespaceMap {
|
|
26
|
+
'dsh-audiogen': AudioGenKey
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SlotMap {
|
|
30
|
+
'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: AudioGenPluginItemOwnerProps }
|
|
31
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: AudioToolViewOwnerProps }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AudioGenPluginItemOwnerProps {
|
|
36
|
+
children?: never
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type AudioToolViewOwnerProps = {
|
|
40
|
+
callId: string
|
|
41
|
+
toolName: string
|
|
42
|
+
block: unknown
|
|
43
|
+
cwd?: string
|
|
44
|
+
home?: string
|
|
45
|
+
openFile: (path: string) => void
|
|
46
|
+
inspect?: () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const inject = ['slots', 'locale', 'connection', 'sessions']
|
|
50
|
+
|
|
51
|
+
export function apply(ctx: ClientContext): void {
|
|
52
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-audiogen: dictionaries')
|
|
53
|
+
registerAudioToolviews(ctx)
|
|
54
|
+
|
|
55
|
+
const connection = ctx.get('connection') as ConnectionHandle | undefined
|
|
56
|
+
const loopback = connection?.isLoopback === true
|
|
57
|
+
const scope: AudiogenScope = bindAudiogenScope(loopback
|
|
58
|
+
? (input, init) => fetch(input, init)
|
|
59
|
+
: () => { throw new Error('settings bridge is loopback-only') })
|
|
60
|
+
|
|
61
|
+
ctx.effect(() => {
|
|
62
|
+
const disposers = [
|
|
63
|
+
ctx.on('connection/reset', () => { void scope.load() }),
|
|
64
|
+
]
|
|
65
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
66
|
+
}, 'dsh-audiogen: settings scope invalidation')
|
|
67
|
+
|
|
68
|
+
const settingsCard = new AudioGenSettingsCardController(scope)
|
|
69
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
70
|
+
name: 'settings.plugin.item',
|
|
71
|
+
key: 'dsh-audiogen',
|
|
72
|
+
locale: NS,
|
|
73
|
+
inject: () => settingsCard.inject(),
|
|
74
|
+
}, AudioGenSettingsCard))
|
|
75
|
+
|
|
76
|
+
let uiDisposer: (() => void) | undefined
|
|
77
|
+
const mountUi = (): void => {
|
|
78
|
+
if (uiDisposer !== undefined) return
|
|
79
|
+
const controller = new AudioGenController()
|
|
80
|
+
const api = new AudiogenApi()
|
|
81
|
+
const disposers: Array<() => void> = []
|
|
82
|
+
try {
|
|
83
|
+
disposers.push(mountSidebarEntry(controller, tt('entry.label'), tt('entry.tooltip')))
|
|
84
|
+
disposers.push(mountPanel(controller, api, scope))
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn('[dsh-audiogen] mount failed:', error)
|
|
87
|
+
}
|
|
88
|
+
uiDisposer = () => {
|
|
89
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
90
|
+
uiDisposer = undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const syncEnabled = (): void => {
|
|
94
|
+
const snapshot = scope.getSnapshot()
|
|
95
|
+
const enabled = snapshot.status === 'ready'
|
|
96
|
+
? snapshot.value?.enabled ?? true
|
|
97
|
+
: snapshot.status === 'unavailable'
|
|
98
|
+
if (enabled) mountUi()
|
|
99
|
+
else uiDisposer?.()
|
|
100
|
+
}
|
|
101
|
+
scope.subscribe(syncEnabled)
|
|
102
|
+
syncEnabled()
|
|
103
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-audiogen surface copy: zh is the key source, en mirrors every key.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const zh = {
|
|
6
|
+
'entry.label': 'AI 音频',
|
|
7
|
+
'entry.tooltip': 'AI 音频面板(TTS / 音乐 / 音效)',
|
|
8
|
+
'panel.title': 'AI 音频',
|
|
9
|
+
'mode.tts': '文本转语音',
|
|
10
|
+
'mode.music': '音乐生成',
|
|
11
|
+
'mode.sfx': '音效生成',
|
|
12
|
+
'prompt.placeholder': '输入要朗读的文本,或描述想生成的音乐 / 音效…',
|
|
13
|
+
'prompt.required': '请输入文本或提示词',
|
|
14
|
+
'model.label': '模型 / 音色',
|
|
15
|
+
'voice.label': '音色',
|
|
16
|
+
'speed.label': '语速',
|
|
17
|
+
'duration.label': '时长(秒)',
|
|
18
|
+
'format.label': '输出格式',
|
|
19
|
+
'generate': '开始生成',
|
|
20
|
+
'generating': '生成中…',
|
|
21
|
+
'download': '下载',
|
|
22
|
+
'result.empty': '生成结果将显示在这里',
|
|
23
|
+
'result.done': '生成完成,共 {count} 段音频',
|
|
24
|
+
'history.title': '历史记录',
|
|
25
|
+
'history.empty': '暂无历史记录',
|
|
26
|
+
'config.missing': '尚未配置音频 API:请前往「设置 → 插件 → AI 音频」添加渠道。',
|
|
27
|
+
'config.disabled': '插件已停用,请在设置中重新启用。',
|
|
28
|
+
'settings.title': 'AI 音频(dsh-audiogen)',
|
|
29
|
+
'settings.description': '配置多厂商音频生成 API 地址与密钥',
|
|
30
|
+
'settings.collapse': '收起',
|
|
31
|
+
'settings.expand': '展开',
|
|
32
|
+
'settings.enabled': '启用插件',
|
|
33
|
+
'settings.announceToAgent': '向 Agent 播报本插件',
|
|
34
|
+
'settings.allowAgentAudio': '允许 Agent 调用音频生成',
|
|
35
|
+
'settings.save': '保存',
|
|
36
|
+
'settings.saving': '保存中…',
|
|
37
|
+
'settings.discard': '放弃修改',
|
|
38
|
+
'settings.unsaved': '有未保存的修改',
|
|
39
|
+
'settings.readOnly': '当前设置为只读。',
|
|
40
|
+
'channels.title': '音频渠道',
|
|
41
|
+
'channels.hint': '每个渠道是一个独立的音频厂商/API 端点,可配置多个。',
|
|
42
|
+
'channels.empty': '还没有渠道。',
|
|
43
|
+
'channels.addProvider': '+ 添加预置厂商',
|
|
44
|
+
'channels.addCustom': '+ 添加自定义渠道',
|
|
45
|
+
'channels.edit': '编辑',
|
|
46
|
+
'channels.delete': '删除',
|
|
47
|
+
'channels.confirm': '确认删除',
|
|
48
|
+
'channels.cancel': '取消',
|
|
49
|
+
'channels.keySet': '已填密钥',
|
|
50
|
+
'channels.keyMissing': '未填密钥',
|
|
51
|
+
'channels.modelCount': '{n} 个模型/音色',
|
|
52
|
+
'channels.noModels': '未配置模型',
|
|
53
|
+
'channels.statusReady': '可用',
|
|
54
|
+
'channels.statusIncomplete': '未完成',
|
|
55
|
+
'channels.untitled': '未命名渠道',
|
|
56
|
+
'channel.name': '名称',
|
|
57
|
+
'channel.apiUrl': 'API 地址',
|
|
58
|
+
'channel.apiKey': 'API 密钥',
|
|
59
|
+
'channel.apiKeyHint': '留空则保持当前密钥;输入新值可更换。',
|
|
60
|
+
'channel.models': '模型 / 音色(每行一个:别名=上游ID)',
|
|
61
|
+
'channel.modelsHint': '例如 tts-1=tts-1 或 Rachel=21m00Tcm4TlvDq8ikWAM',
|
|
62
|
+
'channel.default': '设为默认',
|
|
63
|
+
'channel.cancel': '取消',
|
|
64
|
+
'channel.save': '保存渠道',
|
|
65
|
+
'presets.title': '预置厂商',
|
|
66
|
+
'presets.custom': '自定义渠道',
|
|
67
|
+
} as const
|
|
68
|
+
|
|
69
|
+
export type AudioGenKey = keyof typeof zh
|
|
70
|
+
|
|
71
|
+
export const en: Record<AudioGenKey, string> = {
|
|
72
|
+
'entry.label': 'AI Audio',
|
|
73
|
+
'entry.tooltip': 'AI audio panel (TTS / music / SFX)',
|
|
74
|
+
'panel.title': 'AI Audio',
|
|
75
|
+
'mode.tts': 'Text to speech',
|
|
76
|
+
'mode.music': 'Music',
|
|
77
|
+
'mode.sfx': 'Sound effects',
|
|
78
|
+
'prompt.placeholder': 'Text to speak, or a description of the music / sound effect…',
|
|
79
|
+
'prompt.required': 'Prompt or text is required',
|
|
80
|
+
'model.label': 'Model / voice',
|
|
81
|
+
'voice.label': 'Voice',
|
|
82
|
+
'speed.label': 'Speed',
|
|
83
|
+
'duration.label': 'Duration (s)',
|
|
84
|
+
'format.label': 'Format',
|
|
85
|
+
'generate': 'Generate',
|
|
86
|
+
'generating': 'Generating…',
|
|
87
|
+
'download': 'Download',
|
|
88
|
+
'result.empty': 'Generated audio will appear here.',
|
|
89
|
+
'result.done': 'Done, {count} audio file(s).',
|
|
90
|
+
'history.title': 'History',
|
|
91
|
+
'history.empty': 'No audio history yet.',
|
|
92
|
+
'config.missing': 'No audio API configured. Open Settings > Plugins > AI Audio and add a channel.',
|
|
93
|
+
'config.disabled': 'The plugin is disabled. Enable it in Settings.',
|
|
94
|
+
'settings.title': 'AI Audio (dsh-audiogen)',
|
|
95
|
+
'settings.description': 'Configure multi-vendor audio generation endpoints and keys',
|
|
96
|
+
'settings.collapse': 'Collapse',
|
|
97
|
+
'settings.expand': 'Expand',
|
|
98
|
+
'settings.enabled': 'Enable plugin',
|
|
99
|
+
'settings.announceToAgent': 'Announce this plugin to agents',
|
|
100
|
+
'settings.allowAgentAudio': 'Allow agents to generate audio',
|
|
101
|
+
'settings.save': 'Save',
|
|
102
|
+
'settings.saving': 'Saving…',
|
|
103
|
+
'settings.discard': 'Discard',
|
|
104
|
+
'settings.unsaved': 'Unsaved changes',
|
|
105
|
+
'settings.readOnly': 'Settings are read-only.',
|
|
106
|
+
'channels.title': 'Audio channels',
|
|
107
|
+
'channels.hint': 'Each channel is an independent audio vendor/API endpoint.',
|
|
108
|
+
'channels.empty': 'No channels yet.',
|
|
109
|
+
'channels.addProvider': '+ Add preset vendor',
|
|
110
|
+
'channels.addCustom': '+ Add custom channel',
|
|
111
|
+
'channels.edit': 'Edit',
|
|
112
|
+
'channels.delete': 'Delete',
|
|
113
|
+
'channels.confirm': 'Confirm delete',
|
|
114
|
+
'channels.cancel': 'Cancel',
|
|
115
|
+
'channels.keySet': 'Key set',
|
|
116
|
+
'channels.keyMissing': 'No key',
|
|
117
|
+
'channels.modelCount': '{n} model(s)/voice(s)',
|
|
118
|
+
'channels.noModels': 'No models configured',
|
|
119
|
+
'channels.statusReady': 'Ready',
|
|
120
|
+
'channels.statusIncomplete': 'Incomplete',
|
|
121
|
+
'channels.untitled': 'Untitled channel',
|
|
122
|
+
'channel.name': 'Name',
|
|
123
|
+
'channel.apiUrl': 'API URL',
|
|
124
|
+
'channel.apiKey': 'API key',
|
|
125
|
+
'channel.apiKeyHint': 'Leave blank to keep the current key.',
|
|
126
|
+
'channel.models': 'Models / voices (one per line: alias=upstreamId)',
|
|
127
|
+
'channel.modelsHint': 'e.g. tts-1=tts-1 or Rachel=21m00Tcm4TlvDq8ikWAM',
|
|
128
|
+
'channel.default': 'Set default',
|
|
129
|
+
'channel.cancel': 'Cancel',
|
|
130
|
+
'channel.save': 'Save channel',
|
|
131
|
+
'presets.title': 'Preset vendors',
|
|
132
|
+
'presets.custom': 'Custom channel',
|
|
133
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel view mounting for the AI 音频 panel.
|
|
3
|
+
*
|
|
4
|
+
* Like dsh-imagegen, the panel takes over the center column at the DOM level:
|
|
5
|
+
* a container is appended inside the conversation grid item and a data
|
|
6
|
+
* attribute on <html> hides/shows it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
10
|
+
import type { AudiogenApi } from './api.ts'
|
|
11
|
+
import type { AudioGenController } from './controller.ts'
|
|
12
|
+
import { AudioGenPanel } from './AudioGenPanel.tsx'
|
|
13
|
+
import type { AudiogenScope } from './settings-scope.ts'
|
|
14
|
+
import css from './panel.module.css'
|
|
15
|
+
|
|
16
|
+
export const PANEL_VIEW_SELECTOR = '[data-dsh-audiogen-view]'
|
|
17
|
+
|
|
18
|
+
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
|
|
19
|
+
const ACTIVE_ATTR = 'data-dsh-audiogen-active'
|
|
20
|
+
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active', 'data-dsh-imagegen-active']
|
|
21
|
+
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
22
|
+
const PANEL_NAME = 'audiogen'
|
|
23
|
+
|
|
24
|
+
function conversationColumn(): HTMLElement | undefined {
|
|
25
|
+
return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function mountPanel(
|
|
29
|
+
controller: AudioGenController,
|
|
30
|
+
api: AudiogenApi,
|
|
31
|
+
scope: AudiogenScope,
|
|
32
|
+
): () => void {
|
|
33
|
+
let root: Root | undefined
|
|
34
|
+
let container: HTMLDivElement | undefined
|
|
35
|
+
|
|
36
|
+
const ensure = (): void => {
|
|
37
|
+
if (container !== undefined) {
|
|
38
|
+
if (container.isConnected) return
|
|
39
|
+
root?.unmount()
|
|
40
|
+
root = undefined
|
|
41
|
+
container.remove()
|
|
42
|
+
container = undefined
|
|
43
|
+
}
|
|
44
|
+
const column = conversationColumn()
|
|
45
|
+
if (column === undefined) return
|
|
46
|
+
container = document.createElement('div')
|
|
47
|
+
container.dataset.dshAudiogenView = ''
|
|
48
|
+
container.className = css.view
|
|
49
|
+
column.appendChild(container)
|
|
50
|
+
root = createRoot(container)
|
|
51
|
+
root.render(<AudioGenPanel api={api} scope={scope} />)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const waitObserver = new MutationObserver(() => { ensure() })
|
|
55
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
56
|
+
|
|
57
|
+
const applyActive = (): void => {
|
|
58
|
+
if (controller.getSnapshot().panelOpen) {
|
|
59
|
+
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr)
|
|
60
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
61
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
62
|
+
} else {
|
|
63
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const onOtherActivate = (event: Event): void => {
|
|
67
|
+
const detail = (event as CustomEvent).detail
|
|
68
|
+
if ((detail === 'ssh' || detail === 'taskboard' || detail === 'imagegen') && controller.getSnapshot().panelOpen) {
|
|
69
|
+
controller.close()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
73
|
+
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
74
|
+
if (!controller.getSnapshot().panelOpen) return
|
|
75
|
+
const target = event.target as HTMLElement | null
|
|
76
|
+
if (target === null) return
|
|
77
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
78
|
+
}
|
|
79
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
80
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
81
|
+
const unsubscribe = controller.subscribe(applyActive)
|
|
82
|
+
applyActive()
|
|
83
|
+
ensure()
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
87
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
88
|
+
waitObserver.disconnect()
|
|
89
|
+
unsubscribe()
|
|
90
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
91
|
+
root?.unmount()
|
|
92
|
+
root = undefined
|
|
93
|
+
container?.remove()
|
|
94
|
+
container = undefined
|
|
95
|
+
}
|
|
96
|
+
}
|