@lincyaw/workbench 0.1.0 → 0.2.0-rc.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/README.md +40 -8
- package/package.json +27 -22
- package/src/MediaClipEditor.tsx +122 -0
- package/src/StepProductionEditor.tsx +130 -0
- package/src/approvals.tsx +60 -0
- package/src/assistant-mascot.ts +62 -0
- package/src/audio-recording-messages.ts +44 -0
- package/src/audio-recording.ts +192 -0
- package/src/audio-wav.ts +25 -0
- package/src/audio-waveform.ts +110 -0
- package/src/audition.ts +84 -0
- package/src/authoring-client.ts +136 -0
- package/src/authoring-draft-fragment.ts +28 -0
- package/src/base-controls.tsx +47 -0
- package/src/caption-timing-drafts.ts +61 -0
- package/src/caption-timing-messages.ts +14 -0
- package/src/caption-timing.css +20 -0
- package/src/caption-timing.ts +14 -0
- package/src/component-draft-actions.ts +54 -0
- package/src/component-draft-inspection.ts +35 -0
- package/src/component-properties.css +24 -0
- package/src/component-properties.ts +45 -0
- package/src/component-property-drafts.ts +273 -0
- package/src/component-property-messages.ts +31 -0
- package/src/components.tsx +547 -90
- package/src/context.tsx +27 -0
- package/src/diagnostics.ts +113 -0
- package/src/document-draft-store.ts +179 -0
- package/src/document-drafts.ts +178 -0
- package/src/document-editor/BlockNoteSurface.tsx +209 -0
- package/src/document-editor/ComponentMaterials.tsx +62 -0
- package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
- package/src/document-editor/DocumentBlockFallback.tsx +31 -0
- package/src/document-editor/DocumentBlockPreview.tsx +239 -0
- package/src/document-editor/DocumentComments.tsx +282 -0
- package/src/document-editor/DocumentEditor.tsx +192 -0
- package/src/document-editor/DocumentStepNarration.tsx +93 -0
- package/src/document-editor/NarrationEditor.tsx +110 -0
- package/src/document-editor/NewCommentDrafts.tsx +224 -0
- package/src/document-editor/authoring.tsx +259 -0
- package/src/document-editor/block-playback.ts +90 -0
- package/src/document-editor/block-preview-messages.ts +13 -0
- package/src/document-editor/blocknote-messages.ts +98 -0
- package/src/document-editor/clipboard.ts +40 -0
- package/src/document-editor/colors.ts +57 -0
- package/src/document-editor/comment-draft-actions.ts +58 -0
- package/src/document-editor/comment-draft-inspection.ts +38 -0
- package/src/document-editor/comment-drafts.ts +361 -0
- package/src/document-editor/comments.ts +410 -0
- package/src/document-editor/file-input.ts +71 -0
- package/src/document-editor/focus.ts +54 -0
- package/src/document-editor/inline.ts +104 -0
- package/src/document-editor/math.tsx +63 -0
- package/src/document-editor/messages.ts +14 -0
- package/src/document-editor/narration-schema.tsx +85 -0
- package/src/document-editor/narration.ts +37 -0
- package/src/document-editor/popover.tsx +107 -0
- package/src/document-editor/projection.ts +423 -0
- package/src/document-editor/rich-text.ts +152 -0
- package/src/document-editor/schema.tsx +117 -0
- package/src/document-editor/selection.ts +152 -0
- package/src/document-editor/style.css +9 -0
- package/src/document-editor/styles.d.ts +1 -0
- package/src/document-editor/table-identities.ts +67 -0
- package/src/document-preview.ts +112 -0
- package/src/document-return-point.ts +59 -0
- package/src/document-session.ts +678 -0
- package/src/document-timing-drafts.ts +61 -0
- package/src/document-voices.ts +2 -0
- package/src/history.ts +167 -0
- package/src/index.ts +33 -43
- package/src/jobs.ts +111 -0
- package/src/mascot-preference.ts +37 -0
- package/src/media-drafts.ts +83 -0
- package/src/media-messages.ts +53 -0
- package/src/media-time.ts +12 -0
- package/src/messages.ts +751 -203
- package/src/outline.ts +97 -0
- package/src/panels/ChangeTrail.tsx +79 -0
- package/src/panels/DocumentNarrationPanel.tsx +27 -0
- package/src/panels/DocumentView.tsx +456 -44
- package/src/panels/DocumentVoiceDialog.tsx +179 -0
- package/src/panels/DocumentVoiceField.tsx +35 -0
- package/src/panels/ExportPanel.tsx +133 -0
- package/src/panels/HistoryPanel.tsx +88 -0
- package/src/panels/HomePanel.tsx +184 -0
- package/src/panels/PageDeleteDialog.tsx +33 -0
- package/src/panels/PageRenameDialog.tsx +27 -0
- package/src/panels/PreviewPanel.tsx +79 -0
- package/src/panels/ProductionCaptionTiming.tsx +122 -0
- package/src/panels/ProductionMediaClips.tsx +156 -0
- package/src/panels/ProductionPanel.tsx +272 -0
- package/src/panels/ProductionRecording.tsx +107 -0
- package/src/panels/ProductionRecordingRange.tsx +118 -0
- package/src/panels/ProductionTimeline.tsx +62 -0
- package/src/panels/ProductionTranscription.tsx +140 -0
- package/src/panels/SourceDocumentPanel.tsx +215 -0
- package/src/panels/SpeechSettingsPanel.tsx +64 -0
- package/src/panels/TranscriptionAdoption.tsx +147 -0
- package/src/panels/VoiceCloneDialog.tsx +152 -0
- package/src/panels/VoicePreviewDialog.tsx +74 -0
- package/src/panels/VoiceScopeField.tsx +30 -0
- package/src/panels/VoiceoverDialog.tsx +216 -0
- package/src/places.ts +74 -6
- package/src/preview-playback.ts +125 -0
- package/src/preview-pool.ts +320 -0
- package/src/preview.ts +57 -6
- package/src/production-messages.ts +151 -0
- package/src/production-playback.ts +109 -0
- package/src/production-voice-playback.ts +114 -0
- package/src/recording-adoption.ts +33 -0
- package/src/schema-form/PmSchemaForm.tsx +187 -0
- package/src/schema-form/context.ts +26 -0
- package/src/schema-form/fields.tsx +59 -0
- package/src/schema-form/templates.tsx +50 -0
- package/src/schema-form/validator.ts +36 -0
- package/src/schema-form/widgets.tsx +60 -0
- package/src/shortcuts.ts +8 -0
- package/src/sortable-list.tsx +201 -0
- package/src/speech-settings-messages.ts +40 -0
- package/src/step-production.css +5 -0
- package/src/step-production.ts +51 -0
- package/src/styles.css +757 -361
- package/src/telemetry.ts +152 -0
- package/src/transcription-messages.ts +26 -0
- package/src/transcription-session.ts +159 -0
- package/src/use-recording-adoption.ts +84 -0
- package/src/voice-clone-messages.ts +92 -0
- package/src/voice-drafts.ts +89 -0
- package/src/voice-messages.ts +146 -0
- package/src/voice-snapshot.ts +55 -0
- package/src/voiceover-jobs.ts +87 -0
- package/src/workbench.ts +2328 -0
- package/src/workspace-client.ts +480 -0
- package/src/api.ts +0 -126
- package/src/editor.ts +0 -223
- package/src/merge.ts +0 -30
- package/src/panels/Changes.tsx +0 -128
- package/src/panels/Files.tsx +0 -73
- package/src/panels/Home.tsx +0 -224
- package/src/panels/Preview.tsx +0 -68
- package/src/panels/Problems.tsx +0 -42
- package/src/panels/Source.tsx +0 -118
- package/src/panels/Status.tsx +0 -26
- package/src/react.tsx +0 -59
- package/src/service.ts +0 -82
- package/src/store.ts +0 -599
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Locale } from './messages.ts'
|
|
2
|
+
import { WorkspaceServerError } from './workspace-client.ts'
|
|
3
|
+
import { speechFailureCode } from '@lincyaw/kernel'
|
|
4
|
+
|
|
5
|
+
const zh = {
|
|
6
|
+
voice: '音色', settings: '选择音色', generate: '生成配音', generation: '配音生成与更新',
|
|
7
|
+
currentVoice: '当前音色', unset: '尚未选择音色', inherited: { step: '独立设置', page: '继承页面', document: '继承文稿默认', unset: '未设置' },
|
|
8
|
+
loading: '正在加载音色设置…', loadingVoices: '正在加载可用音色…', loadFailed: '无法加载音色设置,请重试。',
|
|
9
|
+
unsupported: '当前工作区服务尚不支持音色设置。请更新并重新启动 Presmium。',
|
|
10
|
+
noService: '未连接语音服务。',
|
|
11
|
+
speechFailures: {
|
|
12
|
+
'speech-authentication': '语音 API Key 验证失败,请在语音服务设置中更新密钥。',
|
|
13
|
+
'speech-permission': '语音服务拒绝访问,请检查是否已开通语音合成 2.0,以及密钥的服务和音色权限。',
|
|
14
|
+
'speech-quota': '语音服务额度不足,请检查账户余额或可用配额后重试。',
|
|
15
|
+
'speech-busy': '语音服务并发已满,请稍后重新播放。',
|
|
16
|
+
'speech-network': '无法连接语音服务,请检查网络后重新播放。',
|
|
17
|
+
'speech-empty-audio': '语音服务没有返回音频,请重新播放或更换音色。',
|
|
18
|
+
'speech-invalid-request': '语音服务不接受当前讲稿或音色参数,请检查音色和讲稿。',
|
|
19
|
+
'speech-provider': '语音服务暂时无法合成,请稍后重新播放。',
|
|
20
|
+
'speech-cache-failed': '配音缓存无法写入,请检查本机磁盘空间和目录权限。',
|
|
21
|
+
},
|
|
22
|
+
noServiceOption: '尚未连接', savedEngineUnavailable: '已保存的服务(未连接)', generationUnavailable: '当前文稿尚未启用配音生成,讲稿与音色设置仍然保留。',
|
|
23
|
+
catalogUnsupported: '此服务尚未提供音色目录。已保存的音色设置仍然保留。',
|
|
24
|
+
voiceUnavailable: '所选音色不支持此文稿语言或当前服务,请重新选择。', parametersUnsupported: '当前服务不支持所选参数,请刷新音色目录后重新检查。',
|
|
25
|
+
previewTooLong: '当前步骤讲稿超过试听长度,请生成配音后播放。', sourceUnsupported: '此文稿的来源格式暂不支持安全修改音色。原设置和草稿已保留。',
|
|
26
|
+
bundledCatalog: '音色来自服务的内置目录,合成时检查连接。',
|
|
27
|
+
search: '查找音色', searchPlaceholder: '名称、语言或声音来源', available: '可用音色', empty: '没有符合条件的音色',
|
|
28
|
+
results: (count: number) => `${count} 个音色`, language: '语言', allLanguages: '全部语言', provider: '语音服务',
|
|
29
|
+
details: '音色详情', languageMismatch: '不支持文稿的语言', standard: '试听标准示例', currentScript: '试听当前讲稿',
|
|
30
|
+
stop: '停止试听', auditioning: '正在试听', auditionLoading: '正在准备试听…',
|
|
31
|
+
auditionFailed: '无法试听,请重试。音色设置未更改。',
|
|
32
|
+
pauseUnavailable: '预览尚不支持暂停播放,暂时无法试听。请重新加载文稿。',
|
|
33
|
+
pauseTimeout: '无法确认播放已暂停,请重试。',
|
|
34
|
+
changed: '文稿或音色设置已更新。草稿已保留,请按当前步骤重新检查应用范围。',
|
|
35
|
+
draftRecovered: '已恢复尚未应用的音色设置草稿,请检查范围后应用。', recoveryFailed: '无法在此浏览器保留音色草稿,请在关闭前应用或复制设置。',
|
|
36
|
+
recheck: '按当前步骤重新检查', scope: '应用范围', scopes: { step: '当前步骤', steps: '所选步骤', page: '当前页', document: '整份文稿' },
|
|
37
|
+
selectedSteps: '选择步骤', step: (page: number, step: number) => `第 ${page} 页,步骤 ${step}`,
|
|
38
|
+
selected: (count: number) => `已选择 ${count} 个步骤`, inherit: '使用继承的音色', inheritHint: '改用最近的页面或文稿默认音色。',
|
|
39
|
+
pageScript: (page: number) => `第 ${page} 页,关联讲稿(按页生成)`,
|
|
40
|
+
replace: '已有覆盖', preserve: '保留独立设置', replaceAll: '替换独立设置',
|
|
41
|
+
counts: (updated: number, preserved: number) => `将更新 ${updated} 个步骤,保留 ${preserved} 个独立设置。`,
|
|
42
|
+
check: '检查应用范围', checking: '正在检查应用范围…', apply: '应用设置', saved: '音色设置已保存。', cachedPreview: '本次使用已有配音缓存。', clearDefault: '清除文稿默认音色',
|
|
43
|
+
rate: '语速', rateInvalid: (min: number, max: number) => `请输入 ${min}–${max} 之间的数值。`, style: '朗读风格', styleDefault: '服务默认',
|
|
44
|
+
cueStep: '此讲稿包含跨步骤提示,暂不支持单独覆盖音色。可设置页面或文稿默认音色。',
|
|
45
|
+
disclosure: (provider: string, local: boolean) => local ? `本次在本机通过 ${provider} 合成。` : `本次将所示讲稿发送至 ${provider} 合成配音。`,
|
|
46
|
+
confirmPreview: '确认并试听', sampleText: '标准示例文本', source: '声音来源', previewHint: '确认后播放已有缓存或请求合成;选择音色本身不会生成配音。',
|
|
47
|
+
refreshing: '正在加载配音状态…', refresh: '刷新状态', missingOnly: '只生成缺失与需要更新的片段',
|
|
48
|
+
confirmGenerate: '确认并生成', cancelJob: '取消生成', oldVersion: '较早文稿版本', noScript: '当前范围没有可生成的讲稿。',
|
|
49
|
+
statuses: { ready: '可播放', missing: '需要生成', blocked: '暂不可生成', empty: '没有讲稿' },
|
|
50
|
+
jobs: { queued: '等待生成', running: '正在生成', cancelling: '正在取消', cancelled: '已取消', done: '已完成', failed: '生成失败', stale: '文稿已更新' },
|
|
51
|
+
progress: (done: number, total: number, failed: number) => `已完成 ${done} / ${total}${failed ? `,失败 ${failed}` : ''}`,
|
|
52
|
+
generationSummary: (missing: number, blocked: number) => [missing ? `待生成 ${missing} 段` : '', blocked ? `${blocked} 段不可用` : ''].filter(Boolean).join(' · '),
|
|
53
|
+
generationScript: '本次范围的讲稿',
|
|
54
|
+
generationBusy: '此文稿已有生成操作,请等待完成或取消后重试。', cacheReady: '配音已就绪。',
|
|
55
|
+
unboundScript: '此页有关联到跨步骤提示的讲稿,请选择页面或文稿范围生成。',
|
|
56
|
+
recordingUnavailable: '本步使用原音频,无需生成语音。',
|
|
57
|
+
silentScope: (count: number) => `${count} 个步骤已明确设置为静音,设置保持不变。`,
|
|
58
|
+
requestFailed: '无法完成操作。设置和草稿已保留,请重试。', cancel: '取消', close: '关闭', cancelAndClose: '取消并关闭', retry: '重试',
|
|
59
|
+
}
|
|
60
|
+
type VoiceMessages = typeof zh
|
|
61
|
+
const en: VoiceMessages = {
|
|
62
|
+
voice: 'Voice', settings: 'Choose Voice', generate: 'Generate Voiceover', generation: 'Voiceover Generation and Updates',
|
|
63
|
+
currentVoice: 'Current voice', unset: 'No voice selected', inherited: { step: 'Independent setting', page: 'Inherited from page', document: 'Inherited from document', unset: 'Not set' },
|
|
64
|
+
loading: 'Loading voice settings…', loadingVoices: 'Loading available voices…', loadFailed: 'Couldn’t load voice settings. Try again.',
|
|
65
|
+
unsupported: 'This workspace service does not support voice settings yet. Update and restart Presmium.',
|
|
66
|
+
noService: 'No voice service is connected.',
|
|
67
|
+
speechFailures: {
|
|
68
|
+
'speech-authentication': 'The speech API key was rejected. Update it in Voice Service settings.',
|
|
69
|
+
'speech-permission': 'The speech service denied access. Check activation of speech synthesis 2.0 and the key’s service and voice permissions.',
|
|
70
|
+
'speech-quota': 'The speech service has insufficient credit or quota. Check the account and try again.',
|
|
71
|
+
'speech-busy': 'The speech service is at its concurrency limit. Try playing again shortly.',
|
|
72
|
+
'speech-network': 'Couldn’t connect to the speech service. Check the network and play again.',
|
|
73
|
+
'speech-empty-audio': 'The speech service returned no audio. Play again or choose another voice.',
|
|
74
|
+
'speech-invalid-request': 'The speech service rejected this script or voice parameters. Check the voice and script.',
|
|
75
|
+
'speech-provider': 'The speech service can’t generate voiceover right now. Try playing again shortly.',
|
|
76
|
+
'speech-cache-failed': 'Couldn’t write the voiceover cache. Check disk space and directory permissions.',
|
|
77
|
+
},
|
|
78
|
+
noServiceOption: 'Not connected', savedEngineUnavailable: 'Saved service (not connected)', generationUnavailable: 'Voiceover generation is not enabled for this document. Script and voice settings are kept.',
|
|
79
|
+
catalogUnsupported: 'This service does not provide a list of voices. Your saved voice settings are kept.',
|
|
80
|
+
voiceUnavailable: 'The selected voice does not support the document language or service. Choose another voice.', parametersUnsupported: 'This service does not support the selected parameters. Refresh the list of voices and check again.',
|
|
81
|
+
previewTooLong: 'This step’s script exceeds the preview limit. Generate voiceover to play it.', sourceUnsupported: 'The document’s source format cannot safely update voice settings yet. Settings and drafts are kept.',
|
|
82
|
+
bundledCatalog: 'Voices come from the service’s built-in list. The connection is checked when voiceover is generated.',
|
|
83
|
+
search: 'Find a voice', searchPlaceholder: 'Name, language, or voice source', available: 'Available voices', empty: 'No matching voices',
|
|
84
|
+
results: count => `${count} voices`, language: 'Language', allLanguages: 'All languages', provider: 'Voice service',
|
|
85
|
+
details: 'Voice details', languageMismatch: 'Does not support the document language', standard: 'Play Standard Sample', currentScript: 'Play Current Script',
|
|
86
|
+
stop: 'Stop Sample', auditioning: 'Playing', auditionLoading: 'Preparing to play…',
|
|
87
|
+
auditionFailed: 'Couldn’t play the sample. Try again; your voice settings haven’t changed.',
|
|
88
|
+
pauseUnavailable: 'The preview can’t pause playback yet, so listening is unavailable. Reload the document.',
|
|
89
|
+
pauseTimeout: 'Couldn’t confirm that playback paused. Try again.',
|
|
90
|
+
changed: 'The document or voice settings changed. Your draft is kept; check the scope again for the current step.',
|
|
91
|
+
draftRecovered: 'Your draft voice settings were recovered. Check the scope before applying them.', recoveryFailed: 'Voice drafts can’t be kept in this browser. Apply or copy your settings before closing.',
|
|
92
|
+
recheck: 'Check Again for Current Step', scope: 'Apply to', scopes: { step: 'Current step', steps: 'Selected steps', page: 'Current page', document: 'Entire document' },
|
|
93
|
+
selectedSteps: 'Select Steps', step: (page, step) => `Page ${page}, step ${step}`, selected: count => `${count} steps selected`,
|
|
94
|
+
pageScript: page => `Page ${page}, linked script (generate by page)`,
|
|
95
|
+
inherit: 'Use Inherited Voice', inheritHint: 'Use the nearest page or document default voice.',
|
|
96
|
+
replace: 'Existing overrides', preserve: 'Keep independent settings', replaceAll: 'Replace independent settings',
|
|
97
|
+
counts: (updated, preserved) => `${updated} steps will update; ${preserved} independent settings will be kept.`,
|
|
98
|
+
check: 'Check Scope', checking: 'Checking scope…', apply: 'Apply Settings', saved: 'Voice settings saved.', cachedPreview: 'This preview uses an existing voiceover cache.', clearDefault: 'Clear Document Default Voice',
|
|
99
|
+
rate: 'Speaking rate', rateInvalid: (min, max) => `Enter a number between ${min} and ${max}.`, style: 'Reading style', styleDefault: 'Service default',
|
|
100
|
+
cueStep: 'This script has cues across steps. A step voice override is not supported yet; set a page or document default instead.',
|
|
101
|
+
disclosure: (provider, local) => local ? `The voiceover is generated on this device with ${provider}.` : `The script shown is sent to ${provider} to generate the voiceover.`,
|
|
102
|
+
confirmPreview: 'Confirm and Play Sample', sampleText: 'Standard sample text', source: 'Voice source', previewHint: 'After you confirm, a saved voiceover plays or a new one is generated. Choosing a voice doesn’t generate voiceover.',
|
|
103
|
+
refreshing: 'Loading voiceover status…', refresh: 'Refresh Status', missingOnly: 'Generate only missing clips and clips that need updating',
|
|
104
|
+
confirmGenerate: 'Confirm and Generate', cancelJob: 'Cancel Generating', oldVersion: 'Earlier document version', noScript: 'There is no script to generate in this scope.',
|
|
105
|
+
statuses: { ready: 'Playable', missing: 'Needs generation', blocked: 'Unavailable', empty: 'No script' },
|
|
106
|
+
jobs: { queued: 'Queued', running: 'Generating', cancelling: 'Canceling', cancelled: 'Canceled', done: 'Completed', failed: 'Generation failed', stale: 'Document changed' },
|
|
107
|
+
progress: (done, total, failed) => `${done} / ${total} completed${failed ? `; ${failed} failed` : ''}`,
|
|
108
|
+
generationSummary: (missing, blocked) => [missing ? `${missing} ${missing === 1 ? 'clip' : 'clips'} to generate` : '', blocked ? `${blocked} ${blocked === 1 ? 'clip' : 'clips'} unavailable` : ''].filter(Boolean).join(' · '),
|
|
109
|
+
generationScript: 'Script in this scope',
|
|
110
|
+
generationBusy: 'Voiceover is already being generated for this document. Wait for it or cancel it, then try again.', cacheReady: 'Voiceover is ready.',
|
|
111
|
+
unboundScript: 'This page has script with cues across steps. Choose a page or document scope to generate it.',
|
|
112
|
+
recordingUnavailable: 'This step uses original audio, so no voiceover needs to be generated.',
|
|
113
|
+
silentScope: count => `${count} steps are explicitly silent. Their settings are kept.`,
|
|
114
|
+
requestFailed: 'Couldn’t finish the operation. Settings and drafts are kept; try again.', cancel: 'Cancel', close: 'Close', cancelAndClose: 'Cancel and Close', retry: 'Retry',
|
|
115
|
+
}
|
|
116
|
+
export const VOICE_MESSAGES: Record<Locale, VoiceMessages> = { 'zh-CN': zh, en }
|
|
117
|
+
|
|
118
|
+
export function voiceFailure(error: unknown, locale: Locale): string {
|
|
119
|
+
const t = VOICE_MESSAGES[locale]
|
|
120
|
+
const message = error instanceof Error ? error.message : ''
|
|
121
|
+
const speech = speechFailureCode(error)
|
|
122
|
+
if (speech) return (t.speechFailures as Record<string, string>)[speech] ?? t.requestFailed
|
|
123
|
+
if (message === 'not-configured' || message === 'voice-not-configured') return t.noService
|
|
124
|
+
if (message === 'voice-unavailable' || message === 'voice-engine-unsupported' || message === 'voice-engine-unknown') return t.voiceUnavailable
|
|
125
|
+
if (message === 'voice-catalog-unavailable' || message === 'catalog-unavailable') return t.catalogUnsupported
|
|
126
|
+
if (message === 'voice-cue-step') return t.cueStep
|
|
127
|
+
if (message === 'voice-script-empty') return t.noScript
|
|
128
|
+
if (message === 'voice-preview-too-long' || message === 'voice-preview-too-large') return t.previewTooLong
|
|
129
|
+
if (message === 'voice-rate-unsupported' || message === 'voice-style-unsupported') return t.parametersUnsupported
|
|
130
|
+
if (message === 'voice-source-format' || message === 'voice-source-ignored') return t.sourceUnsupported
|
|
131
|
+
if (message === 'voiceover-unavailable') return t.generationUnavailable
|
|
132
|
+
if (message === 'voice-job-busy') return t.generationBusy
|
|
133
|
+
if (message === 'voice-unset') return t.unset
|
|
134
|
+
if (message === 'recording-unavailable' || message === 'voice-recording-unavailable') return t.recordingUnavailable
|
|
135
|
+
if (message === 'speech-unavailable' || message.startsWith('no speech service:')) return t.noService
|
|
136
|
+
if (['voice-source-changed', 'voice-source-unavailable', 'voice-source-pending', 'voice-revision-changed', 'voice-preview-changed', 'voice-step-binding-stale', 'voice-target-changed'].includes(message)) return t.changed
|
|
137
|
+
if (error instanceof WorkspaceServerError) {
|
|
138
|
+
if (error.status === 404) return t.unsupported
|
|
139
|
+
if (error.status === 409) return t.changed
|
|
140
|
+
}
|
|
141
|
+
if (message === 'voice-context-changed') return t.changed
|
|
142
|
+
if (message === 'preview-playback-timeout') return t.pauseTimeout
|
|
143
|
+
if (message.startsWith('preview-playback-')) return t.pauseUnavailable
|
|
144
|
+
if (message.startsWith('audition-')) return t.auditionFailed
|
|
145
|
+
return t.requestFailed
|
|
146
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { voiceTarget } from '@lincyaw/kernel'
|
|
2
|
+
import type { CanonicalSource, IRDocument, VoiceScope, VoiceTarget } from '@lincyaw/kernel'
|
|
3
|
+
import type { DocumentVoiceScope } from './document-voices.ts'
|
|
4
|
+
import { readDocumentSnapshot } from './document-preview.ts'
|
|
5
|
+
import type { Workbench } from './workbench.ts'
|
|
6
|
+
|
|
7
|
+
export interface VoiceSnapshot {
|
|
8
|
+
directory: string
|
|
9
|
+
identity: string
|
|
10
|
+
revision: string
|
|
11
|
+
source: CanonicalSource
|
|
12
|
+
target: VoiceTarget
|
|
13
|
+
ir: IRDocument
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function voiceSnapshotCurrent(workbench: Workbench, snapshot: VoiceSnapshot): boolean {
|
|
17
|
+
const state = workbench.getState(), source = state.authoring
|
|
18
|
+
return state.document === snapshot.directory && source?.identity === snapshot.identity && source.version === snapshot.source.version && source.fingerprint === snapshot.source.fingerprint
|
|
19
|
+
&& source.content?.id === snapshot.source.documentId && !source.loading && !source.dirty && !source.saving && !source.composing && !source.restored && !source.error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function targetFor(ir: IRDocument, stepId: string): VoiceTarget {
|
|
23
|
+
const section = ir.sections.find(page => page.stepIds?.includes(stepId))
|
|
24
|
+
const index = section?.stepIds?.indexOf(stepId) ?? -1
|
|
25
|
+
if (!section || index < 0) throw new Error('voice-step-binding-stale')
|
|
26
|
+
return voiceTarget(section, index)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Uses the acknowledged actor and full runtime step IDs; this request never builds or synthesizes audio. */
|
|
30
|
+
export async function prepareVoiceSnapshot(workbench: Workbench, stepId: string, signal: AbortSignal): Promise<VoiceSnapshot> {
|
|
31
|
+
const initial = workbench.getState(), identity = initial.authoring?.identity, directory = initial.document
|
|
32
|
+
if (!identity || directory === undefined) throw new Error('voice-context-changed')
|
|
33
|
+
await workbench.pausePlayback()
|
|
34
|
+
signal.throwIfAborted()
|
|
35
|
+
await workbench.flushPendingEdits(signal)
|
|
36
|
+
const current = workbench.getState(), source = current.authoring
|
|
37
|
+
if (current.document !== directory || source?.identity !== identity || !source.version || !source.fingerprint || !source.content?.steps[stepId]) throw new Error('voice-context-changed')
|
|
38
|
+
const ir = await readDocumentSnapshot(workbench.client, { directory, version: source.version, fingerprint: source.fingerprint }, signal)
|
|
39
|
+
signal.throwIfAborted()
|
|
40
|
+
if (!ir.doc.source) throw new Error('voice-source-pending')
|
|
41
|
+
const snapshot: VoiceSnapshot = { directory, identity, revision: ir.revision, source: ir.doc.source, target: targetFor(ir.doc, stepId), ir: ir.doc }
|
|
42
|
+
if (!voiceSnapshotCurrent(workbench, snapshot)) throw new Error('voice-context-changed')
|
|
43
|
+
return snapshot
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runtimeVoiceScope(snapshot: VoiceSnapshot, scope: DocumentVoiceScope): VoiceScope {
|
|
47
|
+
if (scope.kind === 'document') return { kind: 'document' }
|
|
48
|
+
if (scope.kind === 'page') {
|
|
49
|
+
if (!snapshot.ir.sections.some(page => page.id === scope.sectionId)) throw new Error('voice-target-changed')
|
|
50
|
+
return { kind: 'page', section: scope.sectionId }
|
|
51
|
+
}
|
|
52
|
+
if (scope.kind === 'step') return { kind: 'step', target: targetFor(snapshot.ir, scope.stepId) }
|
|
53
|
+
if (!scope.stepIds.length) throw new Error('voice-script-empty')
|
|
54
|
+
return { kind: 'steps', targets: [...new Set(scope.stepIds)].map(id => targetFor(snapshot.ir, id)) }
|
|
55
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { errorMessage } from '@lincyaw/kernel'
|
|
2
|
+
import type { VoiceoverJob } from '@lincyaw/kernel'
|
|
3
|
+
import type { WorkbenchStore } from './workbench.ts'
|
|
4
|
+
import type { WorkspaceClient } from './workspace-client.ts'
|
|
5
|
+
|
|
6
|
+
const active = (job: VoiceoverJob) => ['queued', 'running', 'cancelling'].includes(job.state)
|
|
7
|
+
const key = (job: VoiceoverJob) => `${job.id}:${job.sequence}`
|
|
8
|
+
|
|
9
|
+
/** One cheap task observer for the visible production UI, irrespective of who started a job. */
|
|
10
|
+
export class VoiceoverJobs {
|
|
11
|
+
readonly #client: WorkspaceClient
|
|
12
|
+
readonly #store: WorkbenchStore
|
|
13
|
+
readonly #refresh: (job: VoiceoverJob, signal: AbortSignal) => Promise<boolean>
|
|
14
|
+
readonly #observers = new Set<symbol>()
|
|
15
|
+
readonly #settled = new Set<string>()
|
|
16
|
+
readonly #acknowledged = new Map<string, number>()
|
|
17
|
+
#localRevision = 0
|
|
18
|
+
#document?: string
|
|
19
|
+
#controller?: AbortController
|
|
20
|
+
#timer?: ReturnType<typeof setTimeout>
|
|
21
|
+
|
|
22
|
+
constructor(client: WorkspaceClient, store: WorkbenchStore, refresh: (job: VoiceoverJob, signal: AbortSignal) => Promise<boolean>) {
|
|
23
|
+
this.#client = client; this.#store = store; this.#refresh = refresh
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
observe(document: string): () => void {
|
|
27
|
+
if (this.#document !== document) this.stop()
|
|
28
|
+
const token = Symbol('voiceover')
|
|
29
|
+
this.#observers.add(token)
|
|
30
|
+
if (!this.#controller) {
|
|
31
|
+
this.#document = document
|
|
32
|
+
const controller = new AbortController()
|
|
33
|
+
this.#controller = controller
|
|
34
|
+
void this.#poll(document, controller)
|
|
35
|
+
}
|
|
36
|
+
return () => { if (this.#observers.delete(token) && !this.#observers.size) this.stop() }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#current(document: string, controller: AbortController): boolean {
|
|
40
|
+
return this.#controller === controller && !controller.signal.aborted && this.#store.state.document === document
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** An acknowledged local mutation cannot be replaced by an older in-flight list response. */
|
|
44
|
+
remember(document: string, job: VoiceoverJob): void {
|
|
45
|
+
if (this.#document !== document || this.#store.state.document !== document) return
|
|
46
|
+
this.#acknowledged.set(job.id, ++this.#localRevision)
|
|
47
|
+
this.#merge([job])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#merge(incoming: VoiceoverJob[], readRevision?: number): VoiceoverJob[] {
|
|
51
|
+
const previous = this.#store.state.voiceoverJobs ?? [], known = new Map(previous.map(job => [job.id, job]))
|
|
52
|
+
const present = new Set(incoming.map(job => job.id))
|
|
53
|
+
// Only an ACK received after this read began may be absent from its authoritative list.
|
|
54
|
+
const retained = previous.filter(job => !present.has(job.id) && (readRevision === undefined || (this.#acknowledged.get(job.id) ?? 0) > readRevision))
|
|
55
|
+
const jobs = [...retained, ...incoming.map(job => { const old = known.get(job.id); return old && old.sequence > job.sequence ? old : job })]
|
|
56
|
+
const changed = jobs.length !== previous.length || jobs.some((job, index) => key(job) !== key(previous[index]!))
|
|
57
|
+
if (changed || this.#store.state.voiceoverJobsFailure) this.#store.set({ voiceoverJobs: changed ? jobs : previous, voiceoverJobsFailure: undefined })
|
|
58
|
+
return jobs
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async #poll(document: string, controller: AbortController): Promise<void> {
|
|
62
|
+
const signal = controller.signal
|
|
63
|
+
try {
|
|
64
|
+
const readRevision = this.#localRevision, answer = await this.#client.readVoiceoverJobs(document, signal)
|
|
65
|
+
if (!this.#current(document, controller)) return
|
|
66
|
+
const jobs = this.#merge(answer.jobs, readRevision), keys = new Set(jobs.map(key))
|
|
67
|
+
for (const settled of this.#settled) if (!keys.has(settled)) this.#settled.delete(settled)
|
|
68
|
+
for (const [id, revision] of this.#acknowledged) if (revision <= readRevision) this.#acknowledged.delete(id)
|
|
69
|
+
// Unsafe source/recording states leave this work pending; the next cheap read retries its guards.
|
|
70
|
+
for (const job of jobs) {
|
|
71
|
+
if (active(job) || job.completed <= 0 || this.#settled.has(key(job))) continue
|
|
72
|
+
if (await this.#refresh(job, signal)) this.#settled.add(key(job))
|
|
73
|
+
if (!this.#current(document, controller)) return
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (this.#current(document, controller)) this.#store.set({ voiceoverJobsFailure: errorMessage(error) })
|
|
77
|
+
} finally {
|
|
78
|
+
if (this.#current(document, controller)) this.#timer = setTimeout(() => void this.#poll(document, controller), 1000)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
stop(): void {
|
|
83
|
+
this.#controller?.abort(); this.#controller = undefined
|
|
84
|
+
clearTimeout(this.#timer); this.#timer = undefined
|
|
85
|
+
this.#document = undefined; this.#observers.clear(); this.#settled.clear(); this.#acknowledged.clear()
|
|
86
|
+
}
|
|
87
|
+
}
|