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.
@@ -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
+ }