@sutrata/editor 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 +674 -0
- package/dist/ai/OnboardingDialog.d.ts +6 -0
- package/dist/ai/OnboardingDialog.js +115 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
- package/dist/ai/VoiceConfirmDialog.js +37 -0
- package/dist/ai/VoiceToolbar.d.ts +2 -0
- package/dist/ai/VoiceToolbar.js +177 -0
- package/dist/ai/ai-client.d.ts +71 -0
- package/dist/ai/ai-client.js +494 -0
- package/dist/ai/commands.d.ts +12 -0
- package/dist/ai/commands.js +140 -0
- package/dist/ai/generated-providers.json +3696 -0
- package/dist/ai/prompts.d.ts +25 -0
- package/dist/ai/prompts.js +59 -0
- package/dist/ai/voice-service.d.ts +15 -0
- package/dist/ai/voice-service.js +81 -0
- package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
- package/dist/autocomplete/AutocompletePlugin.js +19 -0
- package/dist/autocomplete/suggestions.d.ts +25 -0
- package/dist/autocomplete/suggestions.js +126 -0
- package/dist/context/DocumentContext.d.ts +68 -0
- package/dist/context/DocumentContext.js +605 -0
- package/dist/desktop-api.d.ts +40 -0
- package/dist/editor/EditorView.d.ts +2 -0
- package/dist/editor/EditorView.js +78 -0
- package/dist/editor/SelectionMenu.d.ts +2 -0
- package/dist/editor/SelectionMenu.js +102 -0
- package/dist/editor/attr-pencil-plugin.d.ts +17 -0
- package/dist/editor/attr-pencil-plugin.js +158 -0
- package/dist/editor/character-decoration-plugin.d.ts +26 -0
- package/dist/editor/character-decoration-plugin.js +77 -0
- package/dist/editor/character-view.d.ts +35 -0
- package/dist/editor/character-view.js +112 -0
- package/dist/editor/editor-bus.d.ts +14 -0
- package/dist/editor/editor-bus.js +39 -0
- package/dist/editor/find-highlight-plugin.d.ts +7 -0
- package/dist/editor/find-highlight-plugin.js +57 -0
- package/dist/editor/frontmatter-field-view.d.ts +10 -0
- package/dist/editor/frontmatter-field-view.js +48 -0
- package/dist/editor/frontmatter-field.d.ts +21 -0
- package/dist/editor/frontmatter-field.js +116 -0
- package/dist/editor/grapheme-cursor.d.ts +10 -0
- package/dist/editor/grapheme-cursor.js +104 -0
- package/dist/editor/ime-plugin.d.ts +18 -0
- package/dist/editor/ime-plugin.js +71 -0
- package/dist/editor/insert-helper.d.ts +1 -0
- package/dist/editor/insert-helper.js +42 -0
- package/dist/editor/keymap.d.ts +3 -0
- package/dist/editor/keymap.js +131 -0
- package/dist/editor/language-class-plugin.d.ts +7 -0
- package/dist/editor/language-class-plugin.js +75 -0
- package/dist/editor/plugins.d.ts +1 -0
- package/dist/editor/plugins.js +25 -0
- package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
- package/dist/editor/prosemirror-to-sutra.js +208 -0
- package/dist/editor/scene-heading-view.d.ts +27 -0
- package/dist/editor/scene-heading-view.js +136 -0
- package/dist/editor/schema.d.ts +3 -0
- package/dist/editor/schema.js +135 -0
- package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
- package/dist/editor/sutra-to-prosemirror.js +154 -0
- package/dist/editor/transliterate-input.d.ts +10 -0
- package/dist/editor/transliterate-input.js +88 -0
- package/dist/extensions/storage-adapter.d.ts +59 -0
- package/dist/extensions/storage-adapter.js +9 -0
- package/dist/extensions/types.d.ts +73 -0
- package/dist/extensions/types.js +12 -0
- package/dist/file/docx-exporter.d.ts +8 -0
- package/dist/file/docx-exporter.js +494 -0
- package/dist/file/docx-importer.d.ts +10 -0
- package/dist/file/docx-importer.js +283 -0
- package/dist/file/print-paginator.d.ts +29 -0
- package/dist/file/print-paginator.js +289 -0
- package/dist/file/templates.d.ts +2 -0
- package/dist/file/templates.js +66 -0
- package/dist/file/workflow-reports.d.ts +34 -0
- package/dist/file/workflow-reports.js +719 -0
- package/dist/findreplace/FindReplace.d.ts +2 -0
- package/dist/findreplace/FindReplace.js +164 -0
- package/dist/findreplace/find-engine.d.ts +29 -0
- package/dist/findreplace/find-engine.js +69 -0
- package/dist/findreplace/scroll-to-match.d.ts +29 -0
- package/dist/findreplace/scroll-to-match.js +74 -0
- package/dist/fonts/tofu-detect.d.ts +1 -0
- package/dist/fonts/tofu-detect.js +48 -0
- package/dist/i18n/LanguageContext.d.ts +10 -0
- package/dist/i18n/LanguageContext.js +174 -0
- package/dist/i18n/language-detect.d.ts +1 -0
- package/dist/i18n/language-detect.js +58 -0
- package/dist/i18n/useTranslation.d.ts +11 -0
- package/dist/i18n/useTranslation.js +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/locales/bn.json +143 -0
- package/dist/locales/en.json +143 -0
- package/dist/locales/gu.json +143 -0
- package/dist/locales/hi.json +143 -0
- package/dist/locales/kn.json +143 -0
- package/dist/locales/ml.json +143 -0
- package/dist/locales/mr.json +143 -0
- package/dist/locales/or.json +143 -0
- package/dist/locales/pa.json +143 -0
- package/dist/locales/si.json +143 -0
- package/dist/locales/ta.json +143 -0
- package/dist/locales/te.json +143 -0
- package/dist/navigator/SceneNavigator.d.ts +2 -0
- package/dist/navigator/SceneNavigator.js +612 -0
- package/dist/navigator/StatisticsDialog.d.ts +6 -0
- package/dist/navigator/StatisticsDialog.js +146 -0
- package/dist/navigator/scene-list.d.ts +16 -0
- package/dist/navigator/scene-list.js +70 -0
- package/dist/settings/SettingsDialog.d.ts +6 -0
- package/dist/settings/SettingsDialog.js +147 -0
- package/dist/shell/AppBar.d.ts +2 -0
- package/dist/shell/AppBar.js +34 -0
- package/dist/shell/AppShell.d.ts +2 -0
- package/dist/shell/AppShell.js +30 -0
- package/dist/shell/ConfirmDialog.d.ts +16 -0
- package/dist/shell/ConfirmDialog.js +23 -0
- package/dist/shell/ElementToolbar.d.ts +2 -0
- package/dist/shell/ElementToolbar.js +30 -0
- package/dist/shell/LanguageStatus.d.ts +2 -0
- package/dist/shell/LanguageStatus.js +65 -0
- package/dist/shell/StatusBar.d.ts +2 -0
- package/dist/shell/StatusBar.js +54 -0
- package/dist/shell/SutrataLogo.d.ts +8 -0
- package/dist/shell/SutrataLogo.js +7 -0
- package/dist/shell/Toast.d.ts +12 -0
- package/dist/shell/Toast.js +21 -0
- package/dist/shell/ToolbarButton.d.ts +11 -0
- package/dist/shell/ToolbarButton.js +4 -0
- package/dist/shell/VersionHistory.d.ts +12 -0
- package/dist/shell/VersionHistory.js +89 -0
- package/dist/shell/Watermark.d.ts +3 -0
- package/dist/shell/Watermark.js +10 -0
- package/dist/shell/fonts/fonts.css +1133 -0
- package/dist/shell/icons.d.ts +45 -0
- package/dist/shell/icons.js +51 -0
- package/dist/shell/toolbar-actions.d.ts +6 -0
- package/dist/shell/toolbar-actions.js +58 -0
- package/dist/source/SourceView.d.ts +1 -0
- package/dist/source/SourceView.js +106 -0
- package/dist/source/find-highlight-source.d.ts +14 -0
- package/dist/source/find-highlight-source.js +47 -0
- package/dist/source/sutra-lang.d.ts +2 -0
- package/dist/source/sutra-lang.js +97 -0
- package/dist/spellcheck/dictionaries.d.ts +9 -0
- package/dist/spellcheck/dictionaries.js +16 -0
- package/dist/spellcheck/hunspell.d.ts +23 -0
- package/dist/spellcheck/hunspell.js +94 -0
- package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
- package/dist/spellcheck/spellcheck-plugin.js +75 -0
- package/dist/styles/StyleDialog.d.ts +6 -0
- package/dist/styles/StyleDialog.js +63 -0
- package/dist/styles/builtin-styles.d.ts +43 -0
- package/dist/styles/builtin-styles.js +131 -0
- package/dist/styles/css-adapter.d.ts +32 -0
- package/dist/styles/css-adapter.js +71 -0
- package/dist/styles/docx-adapter.d.ts +7 -0
- package/dist/styles/docx-adapter.js +84 -0
- package/dist/styles/embed-fonts.d.ts +20 -0
- package/dist/styles/embed-fonts.js +81 -0
- package/dist/styles/pagination.d.ts +19 -0
- package/dist/styles/pagination.js +22 -0
- package/dist/styles/print-adapter.d.ts +6 -0
- package/dist/styles/print-adapter.js +288 -0
- package/dist/styles/registry.d.ts +6 -0
- package/dist/styles/registry.js +14 -0
- package/dist/styles/screenplay.css +3006 -0
- package/dist/styles/script-ranges.d.ts +6 -0
- package/dist/styles/script-ranges.js +17 -0
- package/dist/styles/types.d.ts +93 -0
- package/dist/styles/types.js +18 -0
- package/dist/styles/validate.d.ts +12 -0
- package/dist/styles/validate.js +105 -0
- package/dist/templates/templates.d.ts +10 -0
- package/dist/templates/templates.js +181 -0
- package/dist/titlepage/ExportDialog.d.ts +1 -0
- package/dist/titlepage/ExportDialog.js +152 -0
- package/dist/titlepage/TitlePageDialog.d.ts +6 -0
- package/dist/titlepage/TitlePageDialog.js +96 -0
- package/dist/titlepage/frontmatter-form.d.ts +56 -0
- package/dist/titlepage/frontmatter-form.js +245 -0
- package/dist/titlepage/languages.d.ts +10 -0
- package/dist/titlepage/languages.js +31 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
- package/scripts/copy-assets.mjs +16 -0
- package/scripts/generate-providers.mjs +127 -0
- package/src/ai/OnboardingDialog.tsx +390 -0
- package/src/ai/VoiceConfirmDialog.tsx +137 -0
- package/src/ai/VoiceToolbar.tsx +321 -0
- package/src/ai/ai-client.ts +596 -0
- package/src/ai/commands.ts +164 -0
- package/src/ai/generated-providers.json +3696 -0
- package/src/ai/prompts.ts +62 -0
- package/src/ai/voice-service.ts +96 -0
- package/src/autocomplete/AutocompletePlugin.ts +21 -0
- package/src/autocomplete/suggestions.ts +147 -0
- package/src/context/DocumentContext.tsx +648 -0
- package/src/desktop-api.d.ts +40 -0
- package/src/editor/EditorView.tsx +82 -0
- package/src/editor/SelectionMenu.tsx +152 -0
- package/src/editor/attr-pencil-plugin.ts +172 -0
- package/src/editor/character-decoration-plugin.ts +104 -0
- package/src/editor/editor-bus.ts +51 -0
- package/src/editor/find-highlight-plugin.ts +68 -0
- package/src/editor/frontmatter-field-view.ts +52 -0
- package/src/editor/frontmatter-field.ts +115 -0
- package/src/editor/grapheme-cursor.ts +99 -0
- package/src/editor/ime-plugin.ts +85 -0
- package/src/editor/insert-helper.ts +43 -0
- package/src/editor/keymap.ts +124 -0
- package/src/editor/language-class-plugin.ts +91 -0
- package/src/editor/plugins.ts +26 -0
- package/src/editor/prosemirror-to-sutra.ts +192 -0
- package/src/editor/scene-heading-view.ts +147 -0
- package/src/editor/schema.ts +156 -0
- package/src/editor/sutra-to-prosemirror.ts +156 -0
- package/src/extensions/storage-adapter.ts +65 -0
- package/src/extensions/types.ts +60 -0
- package/src/file/docx-exporter.ts +585 -0
- package/src/file/docx-importer.ts +313 -0
- package/src/file/print-paginator.ts +301 -0
- package/src/file/templates.ts +73 -0
- package/src/file/workflow-reports.ts +820 -0
- package/src/findreplace/FindReplace.tsx +242 -0
- package/src/findreplace/find-engine.ts +97 -0
- package/src/findreplace/scroll-to-match.ts +89 -0
- package/src/fonts/tofu-detect.ts +53 -0
- package/src/i18n/LanguageContext.tsx +207 -0
- package/src/i18n/language-detect.ts +73 -0
- package/src/i18n/useTranslation.tsx +74 -0
- package/src/index.ts +27 -0
- package/src/locales/bn.json +143 -0
- package/src/locales/en.json +143 -0
- package/src/locales/gu.json +143 -0
- package/src/locales/hi.json +143 -0
- package/src/locales/kn.json +143 -0
- package/src/locales/ml.json +143 -0
- package/src/locales/mr.json +143 -0
- package/src/locales/or.json +143 -0
- package/src/locales/pa.json +143 -0
- package/src/locales/si.json +143 -0
- package/src/locales/ta.json +143 -0
- package/src/locales/te.json +143 -0
- package/src/navigator/SceneNavigator.tsx +844 -0
- package/src/navigator/StatisticsDialog.tsx +249 -0
- package/src/navigator/scene-list.ts +84 -0
- package/src/settings/SettingsDialog.tsx +303 -0
- package/src/shell/AppBar.tsx +354 -0
- package/src/shell/AppShell.tsx +71 -0
- package/src/shell/ConfirmDialog.tsx +71 -0
- package/src/shell/ElementToolbar.tsx +175 -0
- package/src/shell/LanguageStatus.tsx +101 -0
- package/src/shell/StatusBar.tsx +135 -0
- package/src/shell/SutrataLogo.tsx +89 -0
- package/src/shell/Toast.tsx +46 -0
- package/src/shell/ToolbarButton.tsx +30 -0
- package/src/shell/VersionHistory.tsx +212 -0
- package/src/shell/Watermark.tsx +9 -0
- package/src/shell/fonts/fonts.css +1133 -0
- package/src/shell/icons.tsx +335 -0
- package/src/shell/toolbar-actions.ts +59 -0
- package/src/source/SourceView.tsx +111 -0
- package/src/source/find-highlight-source.ts +57 -0
- package/src/source/sutra-lang.ts +121 -0
- package/src/spellcheck/dictionaries.ts +21 -0
- package/src/spellcheck/hunspell.ts +110 -0
- package/src/spellcheck/spellcheck-plugin.ts +83 -0
- package/src/styles/StyleDialog.tsx +148 -0
- package/src/styles/builtin-styles.ts +139 -0
- package/src/styles/css-adapter.ts +76 -0
- package/src/styles/docx-adapter.ts +109 -0
- package/src/styles/embed-fonts.ts +81 -0
- package/src/styles/pagination.ts +27 -0
- package/src/styles/print-adapter.ts +288 -0
- package/src/styles/registry.ts +20 -0
- package/src/styles/screenplay.css +3006 -0
- package/src/styles/script-ranges.ts +17 -0
- package/src/styles/types.ts +104 -0
- package/src/styles/validate.ts +114 -0
- package/src/templates/templates.ts +194 -0
- package/src/titlepage/ExportDialog.tsx +494 -0
- package/src/titlepage/TitlePageDialog.tsx +312 -0
- package/src/titlepage/frontmatter-form.ts +280 -0
- package/src/titlepage/languages.ts +37 -0
- package/src/types.ts +7 -0
- package/tests/AppBar.test.tsx +35 -0
- package/tests/AppShell.test.tsx +22 -0
- package/tests/ElementToolbar.test.tsx +36 -0
- package/tests/LanguageStatus.test.tsx +129 -0
- package/tests/SelectionMenu.test.tsx +71 -0
- package/tests/ToolbarButton.test.tsx +24 -0
- package/tests/ai-onboarding.test.ts +102 -0
- package/tests/ai-status.test.tsx +72 -0
- package/tests/ai-structured.test.ts +94 -0
- package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
- package/tests/autocomplete/suggestions.test.ts +106 -0
- package/tests/document-context.test.tsx +565 -0
- package/tests/docx-importer.test.ts +160 -0
- package/tests/editor/character-decoration-plugin.test.ts +101 -0
- package/tests/editor/character-extension.test.ts +44 -0
- package/tests/editor/grapheme-cursor.test.ts +118 -0
- package/tests/editor/ime-plugin.test.ts +72 -0
- package/tests/editor/inline-note-bridge.test.ts +32 -0
- package/tests/editor/language-class-plugin.test.ts +265 -0
- package/tests/editor/title-page.test.ts +124 -0
- package/tests/embed-fonts.test.ts +17 -0
- package/tests/exporters.test.ts +365 -0
- package/tests/findreplace/FindReplace.test.tsx +391 -0
- package/tests/findreplace/find-engine.test.ts +86 -0
- package/tests/findreplace/scroll-to-match.test.ts +50 -0
- package/tests/fonts/tofu-detect.test.ts +122 -0
- package/tests/i18n/LanguageContext.test.tsx +146 -0
- package/tests/i18n/language-detect.test.ts +54 -0
- package/tests/i18n/locale-parity.test.ts +25 -0
- package/tests/i18n/useTranslation.test.tsx +45 -0
- package/tests/keymap.test.ts +18 -0
- package/tests/navigator/SceneNavigator.test.tsx +177 -0
- package/tests/navigator/StatisticsDialog.test.tsx +63 -0
- package/tests/navigator/scene-list.test.ts +71 -0
- package/tests/print-paginator.test.ts +82 -0
- package/tests/shell/VersionHistory.test.tsx +129 -0
- package/tests/spellcheck/hunspell.test.ts +82 -0
- package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
- package/tests/styles.test.ts +315 -0
- package/tests/templates/templates.test.ts +83 -0
- package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
- package/tests/titlepage/apply-title-page.test.ts +61 -0
- package/tests/titlepage/frontmatter-form.test.ts +115 -0
- package/tests/toolbar-actions.test.ts +93 -0
- package/tests/voice-commands.test.ts +58 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +65 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import generatedProvidersData from './generated-providers.json';
|
|
2
|
+
/** Compiled-in last resort when both the remote and local models-config.json
|
|
3
|
+
* fetches fail (e.g. fully offline). Generated by scripts/generate-providers.mjs
|
|
4
|
+
* from the same source as models-config.json, so this can no longer drift
|
|
5
|
+
* from the runtime-fetched list the way the old hand-written array did. */
|
|
6
|
+
const FALLBACK_PROVIDERS = generatedProvidersData.providers
|
|
7
|
+
.filter(p => p.essential)
|
|
8
|
+
.map(({ id, name, baseUrl, models }) => ({ id, name, baseUrl, models }));
|
|
9
|
+
export async function getApiKey(provider) {
|
|
10
|
+
if (window.desktopAPI?.getApiKey) {
|
|
11
|
+
try {
|
|
12
|
+
return (await window.desktopAPI.getApiKey(provider)) || null;
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
console.error('Failed to get key from tauri keyring:', e);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return localStorage.getItem(`sutrata_api_key_${provider}`);
|
|
19
|
+
}
|
|
20
|
+
export async function setApiKey(provider, key) {
|
|
21
|
+
if (window.desktopAPI?.setApiKey) {
|
|
22
|
+
try {
|
|
23
|
+
await window.desktopAPI.setApiKey(provider, key);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
console.error('Failed to set key in tauri keyring:', e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
localStorage.setItem(`sutrata_api_key_${provider}`, key);
|
|
31
|
+
}
|
|
32
|
+
export async function deleteApiKey(provider) {
|
|
33
|
+
if (window.desktopAPI?.deleteApiKey) {
|
|
34
|
+
try {
|
|
35
|
+
await window.desktopAPI.deleteApiKey(provider);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error('Failed to delete key from tauri keyring:', e);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
localStorage.removeItem(`sutrata_api_key_${provider}`);
|
|
43
|
+
}
|
|
44
|
+
export function getAIConfig() {
|
|
45
|
+
const stored = localStorage.getItem('sutrata_ai_config');
|
|
46
|
+
if (stored) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(stored);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
// No model id is hardcoded here — callAI/callAIStructured already fall
|
|
56
|
+
// back to the chosen provider's first available model when
|
|
57
|
+
// preferredModel doesn't match one of its models.
|
|
58
|
+
preferredProvider: 'anthropic',
|
|
59
|
+
preferredModel: '',
|
|
60
|
+
customProviders: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function saveAIConfig(config) {
|
|
64
|
+
localStorage.setItem('sutrata_ai_config', JSON.stringify(config));
|
|
65
|
+
}
|
|
66
|
+
let cachedProviders = null;
|
|
67
|
+
export async function fetchProviders(config) {
|
|
68
|
+
if (cachedProviders) {
|
|
69
|
+
return mergeCustomProviders(cachedProviders, config?.customProviders);
|
|
70
|
+
}
|
|
71
|
+
const isProd = import.meta.env?.PROD;
|
|
72
|
+
const githubUrl = 'https://raw.githubusercontent.com/sivarajd/sutrata/main/packages/app/public/models-config.json';
|
|
73
|
+
let providers = [];
|
|
74
|
+
if (isProd) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(githubUrl);
|
|
77
|
+
if (res.ok) {
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
providers = data.providers || [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.warn('Failed to fetch models config from github raw URL, falling back to local:', e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (providers.length === 0) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch('/models-config.json');
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
providers = data.providers || [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.error('Failed to fetch local models-config.json:', e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (providers.length === 0) {
|
|
99
|
+
// Both the remote and local fetches failed (or returned nothing) — use
|
|
100
|
+
// the compiled-in snapshot generated at build time. See FALLBACK_PROVIDERS.
|
|
101
|
+
providers = FALLBACK_PROVIDERS;
|
|
102
|
+
}
|
|
103
|
+
cachedProviders = providers;
|
|
104
|
+
return mergeCustomProviders(providers, config?.customProviders);
|
|
105
|
+
}
|
|
106
|
+
function mergeCustomProviders(base, custom) {
|
|
107
|
+
if (!custom || custom.length === 0)
|
|
108
|
+
return base;
|
|
109
|
+
const list = [...base];
|
|
110
|
+
for (const c of custom) {
|
|
111
|
+
const index = list.findIndex(p => p.id === c.id);
|
|
112
|
+
if (index >= 0) {
|
|
113
|
+
const existing = list[index];
|
|
114
|
+
list[index] = {
|
|
115
|
+
...existing,
|
|
116
|
+
name: c.name,
|
|
117
|
+
baseUrl: c.baseUrl,
|
|
118
|
+
models: c.models,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
list.push({
|
|
123
|
+
id: c.id,
|
|
124
|
+
name: c.name,
|
|
125
|
+
baseUrl: c.baseUrl,
|
|
126
|
+
models: c.models,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return list;
|
|
131
|
+
}
|
|
132
|
+
async function requestHttp(url, method, headers, body) {
|
|
133
|
+
if (window.desktopAPI?.callProviderApi) {
|
|
134
|
+
try {
|
|
135
|
+
const res = await window.desktopAPI.callProviderApi(url, method, headers, body);
|
|
136
|
+
return res;
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
throw new Error(`Desktop API Error: ${e}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(url, {
|
|
145
|
+
method,
|
|
146
|
+
headers,
|
|
147
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
148
|
+
});
|
|
149
|
+
let data;
|
|
150
|
+
const text = await res.text();
|
|
151
|
+
try {
|
|
152
|
+
data = JSON.parse(text);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
data = text;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
status: res.status,
|
|
159
|
+
data,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
throw new Error(`Web Fetch Error: ${e}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const STRUCTURED_TOOL_NAME = 'emit_result';
|
|
168
|
+
/**
|
|
169
|
+
* Anthropic has no OpenAI-compatible endpoint, so it's the only provider
|
|
170
|
+
* that still needs a bespoke request/response shape here — everyone else
|
|
171
|
+
* (including Gemini, via its official /v1beta/openai compat base URL) goes
|
|
172
|
+
* through the generic OpenAI-compatible branch below.
|
|
173
|
+
*/
|
|
174
|
+
async function invokeProvider(provider, model, key, prompt, systemPrompt, responseSchema) {
|
|
175
|
+
let url = '';
|
|
176
|
+
let headers = {};
|
|
177
|
+
let body = null;
|
|
178
|
+
const method = 'POST';
|
|
179
|
+
if (provider.id === 'anthropic') {
|
|
180
|
+
url = `${provider.baseUrl}/messages`;
|
|
181
|
+
headers = {
|
|
182
|
+
'x-api-key': key,
|
|
183
|
+
'anthropic-version': '2023-06-01',
|
|
184
|
+
'content-type': 'application/json',
|
|
185
|
+
};
|
|
186
|
+
body = {
|
|
187
|
+
model,
|
|
188
|
+
max_tokens: 2000,
|
|
189
|
+
system: systemPrompt,
|
|
190
|
+
messages: [
|
|
191
|
+
{ role: 'user', content: prompt }
|
|
192
|
+
],
|
|
193
|
+
temperature: 0.1,
|
|
194
|
+
};
|
|
195
|
+
if (responseSchema) {
|
|
196
|
+
// Force a schema-valid tool call instead of parsing free text —
|
|
197
|
+
// guarantees the fields we asked for, in the shape we asked for.
|
|
198
|
+
body.tools = [{
|
|
199
|
+
name: STRUCTURED_TOOL_NAME,
|
|
200
|
+
description: 'Return the structured result for this request.',
|
|
201
|
+
input_schema: responseSchema,
|
|
202
|
+
strict: true,
|
|
203
|
+
}];
|
|
204
|
+
body.tool_choice = { type: 'tool', name: STRUCTURED_TOOL_NAME };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
url = `${provider.baseUrl}/chat/completions`;
|
|
209
|
+
headers = {
|
|
210
|
+
'Authorization': `Bearer ${key}`,
|
|
211
|
+
'Content-Type': 'application/json',
|
|
212
|
+
};
|
|
213
|
+
const effectiveSystemPrompt = responseSchema
|
|
214
|
+
? `${systemPrompt}\n\n[OUTPUT FORMAT]\nRespond with ONLY a single JSON object matching this JSON schema — no markdown fences, no commentary, no extra text before or after it:\n${JSON.stringify(responseSchema)}`
|
|
215
|
+
: systemPrompt;
|
|
216
|
+
body = {
|
|
217
|
+
model,
|
|
218
|
+
messages: [
|
|
219
|
+
{ role: 'system', content: effectiveSystemPrompt },
|
|
220
|
+
{ role: 'user', content: prompt }
|
|
221
|
+
],
|
|
222
|
+
temperature: 0.1,
|
|
223
|
+
};
|
|
224
|
+
if (responseSchema) {
|
|
225
|
+
// Broadest-compatibility structured-output mode — many self-hosted/
|
|
226
|
+
// aggregator OpenAI-compatible backends don't support the stricter
|
|
227
|
+
// json_schema mode, so this is paired with the prompt instruction
|
|
228
|
+
// above and extractJson()'s tolerant parsing on the caller side.
|
|
229
|
+
body.response_format = { type: 'json_object' };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const res = await requestHttp(url, method, headers, body);
|
|
233
|
+
if (res.status === 429) {
|
|
234
|
+
throw new Error(`Rate limit exceeded (HTTP 429)`);
|
|
235
|
+
}
|
|
236
|
+
if (res.status < 200 || res.status >= 300) {
|
|
237
|
+
const errMsg = typeof res.data === 'object'
|
|
238
|
+
? JSON.stringify(res.data)
|
|
239
|
+
: res.data;
|
|
240
|
+
throw new Error(`HTTP ${res.status}: ${errMsg}`);
|
|
241
|
+
}
|
|
242
|
+
if (provider.id === 'anthropic') {
|
|
243
|
+
if (responseSchema) {
|
|
244
|
+
const toolUse = res.data?.content?.find((block) => block?.type === 'tool_use' && block?.name === STRUCTURED_TOOL_NAME);
|
|
245
|
+
if (!toolUse)
|
|
246
|
+
throw new Error('Invalid response from Anthropic: missing tool_use block');
|
|
247
|
+
return JSON.stringify(toolUse.input);
|
|
248
|
+
}
|
|
249
|
+
const text = res.data?.content?.[0]?.text;
|
|
250
|
+
if (!text)
|
|
251
|
+
throw new Error('Invalid response from Anthropic: missing content text');
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const text = res.data?.choices?.[0]?.message?.content;
|
|
256
|
+
if (!text)
|
|
257
|
+
throw new Error(`Invalid response from ${provider.name}: missing message content`);
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Parses a structured AI response into T, tolerating a model that wraps the
|
|
263
|
+
* JSON in a markdown fence or adds stray text around it despite being asked
|
|
264
|
+
* not to (some providers don't strictly honor response_format hints).
|
|
265
|
+
*/
|
|
266
|
+
export function extractJson(raw) {
|
|
267
|
+
const attempts = [raw.trim()];
|
|
268
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
269
|
+
if (fenceMatch?.[1])
|
|
270
|
+
attempts.push(fenceMatch[1].trim());
|
|
271
|
+
const firstBrace = raw.indexOf('{');
|
|
272
|
+
const lastBrace = raw.lastIndexOf('}');
|
|
273
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
274
|
+
attempts.push(raw.slice(firstBrace, lastBrace + 1));
|
|
275
|
+
}
|
|
276
|
+
for (const attempt of attempts) {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(attempt);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw new Error(`AI did not return valid JSON: ${raw.slice(0, 200)}`);
|
|
285
|
+
}
|
|
286
|
+
export function cleanVoiceResponse(text) {
|
|
287
|
+
let cleaned = text.trim();
|
|
288
|
+
// Strip markdown code block wrapper if present
|
|
289
|
+
cleaned = cleaned.replace(/^```[a-zA-Z0-9]*\n/, '').replace(/\n```$/, '');
|
|
290
|
+
const lines = cleaned.split('\n');
|
|
291
|
+
const filteredLines = lines.map(line => {
|
|
292
|
+
let l = line.trim();
|
|
293
|
+
// Strip leading list bullet (e.g. *, -, +, •) and spaces
|
|
294
|
+
l = l.replace(/^[*•\-+]\s*/, '').trim();
|
|
295
|
+
// Strip leading/trailing backticks if any
|
|
296
|
+
l = l.replace(/^`+/, '').replace(/`+$/, '').trim();
|
|
297
|
+
// Strip common explanatory trailing comments in parens
|
|
298
|
+
l = l.replace(/\s*\(Standard screenplay shorthand[^)]*\)/gi, '');
|
|
299
|
+
l = l.replace(/\s*\(Standard shorthand[^)]*\)/gi, '');
|
|
300
|
+
return l;
|
|
301
|
+
}).filter(line => {
|
|
302
|
+
const trimmed = line.toLowerCase();
|
|
303
|
+
if (!trimmed)
|
|
304
|
+
return false;
|
|
305
|
+
// Discard reasoning/mapping/explanation patterns
|
|
306
|
+
if (trimmed.includes('->') ||
|
|
307
|
+
trimmed.includes('=>') ||
|
|
308
|
+
trimmed.startsWith('input:') ||
|
|
309
|
+
trimmed.startsWith('task:') ||
|
|
310
|
+
trimmed.startsWith('rules:') ||
|
|
311
|
+
trimmed.startsWith('instructions:') ||
|
|
312
|
+
trimmed.startsWith('sutra rules:') ||
|
|
313
|
+
trimmed.startsWith('scene headings:') ||
|
|
314
|
+
trimmed.startsWith('scene id:') ||
|
|
315
|
+
trimmed.startsWith('characters:') ||
|
|
316
|
+
trimmed.startsWith('parentheticals:') ||
|
|
317
|
+
trimmed.startsWith('dialogue:') ||
|
|
318
|
+
trimmed.startsWith('transitions:') ||
|
|
319
|
+
trimmed.startsWith('lyrics:') ||
|
|
320
|
+
trimmed.startsWith('action:') ||
|
|
321
|
+
trimmed.startsWith('format:') ||
|
|
322
|
+
trimmed.startsWith('components:') ||
|
|
323
|
+
trimmed.startsWith('output only') ||
|
|
324
|
+
trimmed.startsWith('no markdown') ||
|
|
325
|
+
trimmed.includes('convert it into valid sutra') ||
|
|
326
|
+
trimmed.includes('sutra syntax rules') ||
|
|
327
|
+
trimmed.includes('clearly a scene heading') ||
|
|
328
|
+
trimmed.includes('screenplay shorthand') ||
|
|
329
|
+
trimmed.includes('append as')) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
});
|
|
334
|
+
return filteredLines.join('\n').trim();
|
|
335
|
+
}
|
|
336
|
+
let nextCallId = 0;
|
|
337
|
+
const activeCalls = new Map();
|
|
338
|
+
const subscribers = new Set();
|
|
339
|
+
export function subscribeToAICalls(callback) {
|
|
340
|
+
subscribers.add(callback);
|
|
341
|
+
callback(Array.from(activeCalls.values()));
|
|
342
|
+
return () => {
|
|
343
|
+
subscribers.delete(callback);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function notifySubscribers() {
|
|
347
|
+
const list = Array.from(activeCalls.values());
|
|
348
|
+
for (const sub of subscribers) {
|
|
349
|
+
sub(list);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export function formatActiveCalls(calls) {
|
|
353
|
+
if (calls.length === 0)
|
|
354
|
+
return '';
|
|
355
|
+
if (calls.length === 1) {
|
|
356
|
+
return `Calling ${calls[0].providerName} (${calls[0].model})...`;
|
|
357
|
+
}
|
|
358
|
+
const unique = Array.from(new Set(calls.map(c => `${c.providerName} (${c.model})`)));
|
|
359
|
+
if (unique.length === 1) {
|
|
360
|
+
return `Calling ${unique[0]} (${calls.length} calls)...`;
|
|
361
|
+
}
|
|
362
|
+
return `Calling ${unique.join(', ')}...`;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Resolves the provider/key/model fallback chain and invokes it, retrying
|
|
366
|
+
* the next configured provider on failure. Shared by callAI and
|
|
367
|
+
* callAIStructured so the two don't duplicate this logic.
|
|
368
|
+
*/
|
|
369
|
+
async function resolveAndInvoke(prompt, systemPrompt, config, responseSchema) {
|
|
370
|
+
const providers = await fetchProviders(config);
|
|
371
|
+
const availableProviders = [];
|
|
372
|
+
for (const p of providers) {
|
|
373
|
+
const key = await getApiKey(p.id);
|
|
374
|
+
if (key) {
|
|
375
|
+
availableProviders.push(p.id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (availableProviders.length === 0) {
|
|
379
|
+
throw new Error('No AI API keys configured. Please configure keys in Settings.');
|
|
380
|
+
}
|
|
381
|
+
let providersToTry = [config.preferredProvider, ...availableProviders.filter(p => p !== config.preferredProvider)];
|
|
382
|
+
providersToTry = providersToTry.filter(p => availableProviders.includes(p));
|
|
383
|
+
if (providersToTry.length === 0) {
|
|
384
|
+
providersToTry = [availableProviders[0]];
|
|
385
|
+
}
|
|
386
|
+
let lastError = '';
|
|
387
|
+
for (const providerId of providersToTry) {
|
|
388
|
+
const provider = providers.find(p => p.id === providerId);
|
|
389
|
+
if (!provider)
|
|
390
|
+
continue;
|
|
391
|
+
const key = await getApiKey(providerId);
|
|
392
|
+
if (!key)
|
|
393
|
+
continue;
|
|
394
|
+
// config.preferredModel is only meaningful for config.preferredProvider
|
|
395
|
+
// — Settings/Onboarding always save them together, and the model is
|
|
396
|
+
// picked from that provider's *live* listModels() result, which is
|
|
397
|
+
// usually not a member of `provider.models` (the static generated
|
|
398
|
+
// list, kept only as a last-resort default). Validating it against
|
|
399
|
+
// that static list here would silently discard a perfectly valid
|
|
400
|
+
// live-picked model and substitute a possibly-stale/retired one
|
|
401
|
+
// instead. For any other (fallback) provider in this loop,
|
|
402
|
+
// preferredModel genuinely doesn't apply, so fall back to its first
|
|
403
|
+
// static model.
|
|
404
|
+
const model = providerId === config.preferredProvider
|
|
405
|
+
? (config.preferredModel || provider.models[0]?.id || '')
|
|
406
|
+
: (provider.models[0]?.id || '');
|
|
407
|
+
const callId = `call-${nextCallId++}`;
|
|
408
|
+
activeCalls.set(callId, { providerName: provider.name, model });
|
|
409
|
+
notifySubscribers();
|
|
410
|
+
try {
|
|
411
|
+
return await invokeProvider(provider, model, key, prompt, systemPrompt, responseSchema);
|
|
412
|
+
}
|
|
413
|
+
catch (e) {
|
|
414
|
+
console.warn(`Provider ${providerId} failed:`, e);
|
|
415
|
+
lastError = `[${provider.name} / ${model}] ${e.message || String(e)}`;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
activeCalls.delete(callId);
|
|
420
|
+
notifySubscribers();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`AI generation failed. Last error: ${lastError}`);
|
|
424
|
+
}
|
|
425
|
+
export async function callAI(prompt, systemPrompt, config, options) {
|
|
426
|
+
const response = await resolveAndInvoke(prompt, systemPrompt, config);
|
|
427
|
+
return options?.clean === 'voice' ? cleanVoiceResponse(response) : response;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Like callAI, but requests and validates a JSON response matching `schema`
|
|
431
|
+
* instead of returning free text — the explicit contract that replaced
|
|
432
|
+
* sniffing the system prompt for keywords like "duration"/"synopsis".
|
|
433
|
+
*/
|
|
434
|
+
export async function callAIStructured(prompt, systemPrompt, config, schema) {
|
|
435
|
+
const response = await resolveAndInvoke(prompt, systemPrompt, config, schema);
|
|
436
|
+
return extractJson(response);
|
|
437
|
+
}
|
|
438
|
+
/** Any provider (from the fetched + custom list) with a stored API key. */
|
|
439
|
+
export async function hasConfiguredApiKey(config) {
|
|
440
|
+
const providers = await fetchProviders(config);
|
|
441
|
+
for (const p of providers) {
|
|
442
|
+
if (await getApiKey(p.id))
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Fetches the provider's live model list so Settings/Onboarding show
|
|
449
|
+
* current models instead of a potentially stale static one. Never throws —
|
|
450
|
+
* falls back to `provider.models` on any error (missing endpoint, bad key,
|
|
451
|
+
* network failure), since this is a progressive enhancement.
|
|
452
|
+
*/
|
|
453
|
+
export async function listModels(provider, apiKey) {
|
|
454
|
+
try {
|
|
455
|
+
const isAnthropic = provider.id === 'anthropic';
|
|
456
|
+
const url = isAnthropic ? 'https://api.anthropic.com/v1/models' : `${provider.baseUrl}/models`;
|
|
457
|
+
const headers = isAnthropic
|
|
458
|
+
? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }
|
|
459
|
+
: { 'Authorization': `Bearer ${apiKey}` };
|
|
460
|
+
const res = await requestHttp(url, 'GET', headers, null);
|
|
461
|
+
if (res.status < 200 || res.status >= 300) {
|
|
462
|
+
throw new Error(`HTTP ${res.status}`);
|
|
463
|
+
}
|
|
464
|
+
const data = res.data?.data;
|
|
465
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
466
|
+
throw new Error('Provider returned an empty model list');
|
|
467
|
+
}
|
|
468
|
+
return data.map((m) => ({
|
|
469
|
+
id: m.id,
|
|
470
|
+
name: m.display_name || m.id,
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
console.warn(`Failed to list live models for ${provider.id}, using static list:`, e);
|
|
475
|
+
return provider.models;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
export async function testConnection(providerId, apiKey, config) {
|
|
479
|
+
const providers = await fetchProviders(config);
|
|
480
|
+
const provider = providers.find(p => p.id === providerId);
|
|
481
|
+
if (!provider)
|
|
482
|
+
throw new Error('Unknown provider');
|
|
483
|
+
const liveModels = await listModels(provider, apiKey);
|
|
484
|
+
const testModel = liveModels[0]?.id;
|
|
485
|
+
if (!testModel)
|
|
486
|
+
throw new Error('No models found for provider');
|
|
487
|
+
const resText = await invokeProvider(provider, testModel, apiKey, "Hello, reply with one word: OK", "Connection test verification.");
|
|
488
|
+
return resText.trim().length > 0;
|
|
489
|
+
}
|
|
490
|
+
export function mockActiveCallsList(calls) {
|
|
491
|
+
activeCalls.clear();
|
|
492
|
+
calls.forEach((c, idx) => activeCalls.set(`mock-${idx}`, c));
|
|
493
|
+
notifySubscribers();
|
|
494
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type CommandType = 'scene' | 'character' | 'dialogue' | 'parenthetical' | 'transition' | 'lyrics' | 'action' | 'literal' | 'plain';
|
|
2
|
+
export interface CommandDefinition {
|
|
3
|
+
type: CommandType;
|
|
4
|
+
keywords: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare const LOCALIZED_COMMANDS: Record<string, CommandDefinition[]>;
|
|
7
|
+
export interface ParsedVoiceResult {
|
|
8
|
+
type: CommandType | 'action';
|
|
9
|
+
text: string;
|
|
10
|
+
isLiteral: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseVoiceCommand(transcript: string, langCode: string): ParsedVoiceResult;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export const LOCALIZED_COMMANDS = {
|
|
2
|
+
en: [
|
|
3
|
+
{ type: 'literal', keywords: ['literal', 'plain', 'bypass'] },
|
|
4
|
+
{ type: 'scene', keywords: ['new scene', 'scene heading', 'scene'] },
|
|
5
|
+
{ type: 'character', keywords: ['character name', 'character', 'char'] },
|
|
6
|
+
{ type: 'dialogue', keywords: ['dialogue', 'dialog', 'says'] },
|
|
7
|
+
{ type: 'parenthetical', keywords: ['parenthetical', 'bracket', 'brackets'] },
|
|
8
|
+
{ type: 'transition', keywords: ['transition', 'fade out', 'fade in', 'cut to'] },
|
|
9
|
+
{ type: 'lyrics', keywords: ['lyrics', 'sing lyrics', 'song'] },
|
|
10
|
+
{ type: 'action', keywords: ['action description', 'action'] },
|
|
11
|
+
],
|
|
12
|
+
hi: [
|
|
13
|
+
{ type: 'literal', keywords: ['शब्दशः', 'वास्तविक', 'प्लेन'] },
|
|
14
|
+
{ type: 'scene', keywords: ['नया दृश्य', 'दृश्य'] },
|
|
15
|
+
{ type: 'character', keywords: ['पात्र नाम', 'पात्र', 'चरित्र'] },
|
|
16
|
+
{ type: 'dialogue', keywords: ['संवाद', 'बोलता है', 'बोलती है'] },
|
|
17
|
+
{ type: 'parenthetical', keywords: ['कोष्ठक', 'ब्रैकेट'] },
|
|
18
|
+
{ type: 'transition', keywords: ['बदलाव', 'कट टू'] },
|
|
19
|
+
{ type: 'lyrics', keywords: ['गीत', 'गाना'] },
|
|
20
|
+
{ type: 'action', keywords: ['विवरण', 'एक्शन'] },
|
|
21
|
+
],
|
|
22
|
+
ta: [
|
|
23
|
+
{ type: 'literal', keywords: ['உண்மையான', 'வார்த்தை', 'நேரடி'] },
|
|
24
|
+
{ type: 'scene', keywords: ['புதிய காட்சி', 'காட்சி'] },
|
|
25
|
+
{ type: 'character', keywords: ['கதாபாத்திரம் பெயர்', 'கதாபாத்திரம்'] },
|
|
26
|
+
{ type: 'dialogue', keywords: ['வசனம்', 'பேசுகிறார்'] },
|
|
27
|
+
{ type: 'parenthetical', keywords: ['அடைப்புக்குறி', 'பிராக்கெட்'] },
|
|
28
|
+
{ type: 'transition', keywords: ['மாற்றம்', 'கட் டூ'] },
|
|
29
|
+
{ type: 'lyrics', keywords: ['பாடல்', 'பாட்டு'] },
|
|
30
|
+
{ type: 'action', keywords: ['செயல்', 'விளக்கம்'] },
|
|
31
|
+
],
|
|
32
|
+
te: [
|
|
33
|
+
{ type: 'literal', keywords: ['యథార్థం', 'సాధారణ'] },
|
|
34
|
+
{ type: 'scene', keywords: ['కొత్త సన్నివేశం', 'సన్నివేశం'] },
|
|
35
|
+
{ type: 'character', keywords: ['పాత్ర పేరు', 'పాత్ర'] },
|
|
36
|
+
{ type: 'dialogue', keywords: ['సంభాషణ', 'మాట్లాడు'] },
|
|
37
|
+
{ type: 'parenthetical', keywords: ['బ్రాకెట్', 'వరణము'] },
|
|
38
|
+
{ type: 'transition', keywords: ['మార్పు', 'కట్ టు'] },
|
|
39
|
+
{ type: 'lyrics', keywords: ['పాట', 'గీతం'] },
|
|
40
|
+
{ type: 'action', keywords: ['చర్య', 'వివరణ'] },
|
|
41
|
+
],
|
|
42
|
+
ml: [
|
|
43
|
+
{ type: 'literal', keywords: ['യഥാർത്ഥ', 'സാധാരണ'] },
|
|
44
|
+
{ type: 'scene', keywords: ['പുതിയ രംഗം', 'രംഗം'] },
|
|
45
|
+
{ type: 'character', keywords: ['കഥാപാത്രം പേര്', 'കഥാപാത്രം'] },
|
|
46
|
+
{ type: 'dialogue', keywords: ['സംഭാഷണം', 'പറയുന്നു'] },
|
|
47
|
+
{ type: 'parenthetical', keywords: ['ബ്രാക്കറ്റ്'] },
|
|
48
|
+
{ type: 'transition', keywords: ['മാറ്റം', 'കട്ട് ടു'] },
|
|
49
|
+
{ type: 'lyrics', keywords: ['പാട്ട്', 'ഗാനം'] },
|
|
50
|
+
{ type: 'action', keywords: ['പ്രവർത്തി', 'വിവരണം'] },
|
|
51
|
+
],
|
|
52
|
+
kn: [
|
|
53
|
+
{ type: 'literal', keywords: ['ನಿಜವಾದ', 'ಸಾಮಾನ್ಯ'] },
|
|
54
|
+
{ type: 'scene', keywords: ['ಹೊಸ ದೃಶ್ಯ', 'ದೃಶ್ಯ'] },
|
|
55
|
+
{ type: 'character', keywords: ['ಪಾತ್ರ ಹೆಸರು', 'ಪಾತ್ರ'] },
|
|
56
|
+
{ type: 'dialogue', keywords: ['ಸಂಭಾಷಣೆ', 'ಹೇಳುತ್ತದೆ'] },
|
|
57
|
+
{ type: 'parenthetical', keywords: ['ವರಣಚಿಹ್ನೆ', 'ಬ್ರಾಕೆಟ್'] },
|
|
58
|
+
{ type: 'transition', keywords: ['ಬದಲಾವಣೆ', 'ಕಟ್ ಟು'] },
|
|
59
|
+
{ type: 'lyrics', keywords: ['ಹಾಡು', 'ಗೀತೆ'] },
|
|
60
|
+
{ type: 'action', keywords: ['ಕ್ರಿಯೆ', 'ವಿವರಣೆ'] },
|
|
61
|
+
],
|
|
62
|
+
bn: [
|
|
63
|
+
{ type: 'literal', keywords: ['আক্ষরিক', 'সাধারণ'] },
|
|
64
|
+
{ type: 'scene', keywords: ['নতুন দৃশ্য', 'দৃশ্য'] },
|
|
65
|
+
{ type: 'character', keywords: ['চরিত্রের নাম', 'চরিত্র'] },
|
|
66
|
+
{ type: 'dialogue', keywords: ['সংলাপ', 'বলে'] },
|
|
67
|
+
{ type: 'parenthetical', keywords: ['বন্ধনী', 'ব্র্যাকেট'] },
|
|
68
|
+
{ type: 'transition', keywords: ['পরিবর্তন', 'কাট টু'] },
|
|
69
|
+
{ type: 'lyrics', keywords: ['গান', 'গীতি'] },
|
|
70
|
+
{ type: 'action', keywords: ['ক্রিয়া', 'বর্ণনা'] },
|
|
71
|
+
],
|
|
72
|
+
gu: [
|
|
73
|
+
{ type: 'literal', keywords: ['શાબ્દિક', 'સામાન્ય'] },
|
|
74
|
+
{ type: 'scene', keywords: ['નવું દ્રશ્ય', 'દ્રશ્ય'] },
|
|
75
|
+
{ type: 'character', keywords: ['પાત્રનું નામ', 'પાત્ર'] },
|
|
76
|
+
{ type: 'dialogue', keywords: ['સંવાદ', 'બોલે છે'] },
|
|
77
|
+
{ type: 'parenthetical', keywords: ['કૌંસ', 'બ્રેકેટ'] },
|
|
78
|
+
{ type: 'transition', keywords: ['પરિવર્તન', 'કટ ટુ'] },
|
|
79
|
+
{ type: 'lyrics', keywords: ['ગીત', 'ગાણું'] },
|
|
80
|
+
{ type: 'action', keywords: ['ક્રિયા', 'વર્ણન'] },
|
|
81
|
+
],
|
|
82
|
+
mr: [
|
|
83
|
+
{ type: 'literal', keywords: ['शाब्दिक', 'साधे'] },
|
|
84
|
+
{ type: 'scene', keywords: ['नवीन दृश्य', 'दृश्य'] },
|
|
85
|
+
{ type: 'character', keywords: ['पात्राचे नाव', 'पात्र'] },
|
|
86
|
+
{ type: 'dialogue', keywords: ['संवाद', 'बोलतो', 'बोलते'] },
|
|
87
|
+
{ type: 'parenthetical', keywords: ['कंसात', 'ब्रॅकेट'] },
|
|
88
|
+
{ type: 'transition', keywords: ['बदल', 'कट टू'] },
|
|
89
|
+
{ type: 'lyrics', keywords: ['गाणे', 'गीत'] },
|
|
90
|
+
{ type: 'action', keywords: ['कृती', 'वर्णन'] },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
export function parseVoiceCommand(transcript, langCode) {
|
|
94
|
+
const normText = transcript.trim();
|
|
95
|
+
// Get language mapping, default to English if not found
|
|
96
|
+
const baseLang = langCode.split('-')[0] || 'en';
|
|
97
|
+
const defs = LOCALIZED_COMMANDS[baseLang] || LOCALIZED_COMMANDS.en;
|
|
98
|
+
// 1. Check for literal bypass sequence
|
|
99
|
+
const literalDef = defs.find(d => d.type === 'literal');
|
|
100
|
+
if (literalDef) {
|
|
101
|
+
for (const kw of literalDef.keywords) {
|
|
102
|
+
if (normText.toLowerCase().startsWith(kw + ' ')) {
|
|
103
|
+
return {
|
|
104
|
+
type: 'action',
|
|
105
|
+
text: normText.substring(kw.length + 1).trim(),
|
|
106
|
+
isLiteral: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 2. Scan other formatting keywords
|
|
112
|
+
for (const def of defs) {
|
|
113
|
+
if (def.type === 'literal')
|
|
114
|
+
continue;
|
|
115
|
+
for (const kw of def.keywords) {
|
|
116
|
+
// Check if text starts with the keyword followed by space or is exactly the keyword
|
|
117
|
+
const prefix = kw + ' ';
|
|
118
|
+
if (normText.toLowerCase().startsWith(prefix)) {
|
|
119
|
+
return {
|
|
120
|
+
type: def.type,
|
|
121
|
+
text: normText.substring(kw.length + 1).trim(),
|
|
122
|
+
isLiteral: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else if (normText.toLowerCase() === kw) {
|
|
126
|
+
return {
|
|
127
|
+
type: def.type,
|
|
128
|
+
text: '',
|
|
129
|
+
isLiteral: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 3. Fallback to default action text
|
|
135
|
+
return {
|
|
136
|
+
type: 'action',
|
|
137
|
+
text: normText,
|
|
138
|
+
isLiteral: false,
|
|
139
|
+
};
|
|
140
|
+
}
|