synthos 0.7.2 → 0.9.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 +215 -65
- package/default-pages/application/page.html +42 -0
- package/default-pages/application/page.json +10 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
- package/default-pages/elevenlabs_effects_studio/page.json +11 -0
- package/default-pages/elevenlabs_voice_studio/page.html +801 -0
- package/default-pages/elevenlabs_voice_studio/page.json +11 -0
- package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
- package/default-pages/json_tools/page.json +10 -0
- package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
- package/default-pages/my_notes/page.html +132 -0
- package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
- package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
- package/default-pages/neon_asteroids/files/effects.json +74 -0
- package/default-pages/neon_asteroids/page.html +1822 -0
- package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
- package/default-pages/oregon_trail/page.html +323 -0
- package/default-pages/oregon_trail/page.json +12 -0
- package/default-pages/retro_game_starter/page.html +1308 -0
- package/default-pages/retro_game_starter/page.json +12 -0
- package/default-pages/{sidebar_builder.html → sidebar_page/page.html} +12 -10
- package/default-pages/sidebar_page/page.json +10 -0
- package/default-pages/{solar_explorer.html → solar_explorer/page.html} +24 -29
- package/default-pages/{solar_explorer.json → solar_explorer/page.json} +4 -4
- package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
- package/default-pages/solar_tutorial/page.json +10 -0
- package/default-pages/{two-panel_builder.html → two-panel_page/page.html} +13 -11
- package/default-pages/two-panel_page/page.json +10 -0
- package/default-pages/us_map/page.html +193 -0
- package/default-pages/us_map/page.json +12 -0
- package/default-pages/us_map_1850/page.html +326 -0
- package/default-pages/us_map_1850/page.json +12 -0
- package/default-pages/western_cities_1850/page.html +527 -0
- package/default-pages/western_cities_1850/page.json +12 -0
- package/default-themes/aurora-dawn.json +19 -0
- package/default-themes/aurora-dawn.v3.css +198 -0
- package/default-themes/aurora-dusk.json +19 -0
- package/default-themes/aurora-dusk.v3.css +200 -0
- package/default-themes/cosmos-dawn.json +19 -0
- package/default-themes/cosmos-dawn.v3.css +198 -0
- package/default-themes/cosmos-dusk.json +19 -0
- package/default-themes/cosmos-dusk.v3.css +200 -0
- package/default-themes/high-contrast-dark.json +19 -0
- package/default-themes/high-contrast-dark.v3.css +200 -0
- package/default-themes/high-contrast-light.json +19 -0
- package/default-themes/high-contrast-light.v3.css +198 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +134 -0
- package/default-themes/nebula-dawn.v3.css +199 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +128 -0
- package/default-themes/nebula-dusk.v3.css +201 -0
- package/default-themes/solar-flare-dawn.json +19 -0
- package/default-themes/solar-flare-dawn.v3.css +198 -0
- package/default-themes/solar-flare-dusk.json +19 -0
- package/default-themes/solar-flare-dusk.v3.css +200 -0
- package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
- package/dist/agents/a2a/a2aProvider.js +126 -0
- package/dist/agents/a2a/a2aProvider.js.map +1 -0
- package/dist/agents/discovery.d.ts.map +1 -0
- package/dist/agents/discovery.js +52 -0
- package/dist/agents/discovery.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +20 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +117 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +486 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +237 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +25 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +359 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/builders/anthropic.d.ts +31 -0
- package/dist/builders/anthropic.d.ts.map +1 -0
- package/dist/builders/anthropic.js +227 -0
- package/dist/builders/anthropic.js.map +1 -0
- package/dist/builders/fireworksai.d.ts +9 -0
- package/dist/builders/fireworksai.d.ts.map +1 -0
- package/dist/builders/fireworksai.js +57 -0
- package/dist/builders/fireworksai.js.map +1 -0
- package/dist/builders/index.d.ts +13 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +31 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/builders/openai.d.ts +8 -0
- package/dist/builders/openai.d.ts.map +1 -0
- package/dist/builders/openai.js +87 -0
- package/dist/builders/openai.js.map +1 -0
- package/dist/builders/types.d.ts +54 -0
- package/dist/builders/types.d.ts.map +1 -0
- package/dist/builders/types.js +211 -0
- package/dist/builders/types.js.map +1 -0
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +3 -2
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/registry.d.ts +2 -1
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +65 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/customizer/Customizer.d.ts +57 -0
- package/dist/customizer/Customizer.d.ts.map +1 -0
- package/dist/customizer/Customizer.js +124 -0
- package/dist/customizer/Customizer.js.map +1 -0
- package/dist/customizer/index.d.ts.map +1 -0
- package/dist/customizer/index.js +9 -0
- package/dist/customizer/index.js.map +1 -0
- package/dist/files.d.ts +17 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +75 -1
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +10 -6
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +97 -86
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +142 -145
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +24 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +103 -0
- package/dist/models/anthropic.js.map +1 -0
- package/dist/models/chainOfThought.d.ts.map +1 -0
- package/dist/models/chainOfThought.js +45 -0
- package/dist/models/chainOfThought.js.map +1 -0
- package/dist/models/fireworksai.d.ts.map +1 -0
- package/dist/models/fireworksai.js +141 -0
- package/dist/models/fireworksai.js.map +1 -0
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +20 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/logCompletePrompt.d.ts.map +1 -0
- package/dist/models/logCompletePrompt.js +23 -0
- package/dist/models/logCompletePrompt.js.map +1 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.d.ts.map +1 -0
- package/dist/models/openai.js +101 -0
- package/dist/models/openai.js.map +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +53 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +21 -0
- package/dist/models/types.js.map +1 -1
- package/dist/models/utils.d.ts.map +1 -0
- package/dist/models/utils.js +21 -0
- package/dist/models/utils.js.map +1 -0
- package/dist/pages.d.ts +30 -7
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +177 -55
- package/dist/pages.js.map +1 -1
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +4 -3
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/generateImage.d.ts.map +1 -1
- package/dist/service/generateImage.js +3 -3
- package/dist/service/generateImage.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +39 -7
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +47 -18
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +559 -270
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +5 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +392 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +380 -138
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +20 -9
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useFileRoutes.d.ts +4 -0
- package/dist/service/useFileRoutes.d.ts.map +1 -0
- package/dist/service/useFileRoutes.js +122 -0
- package/dist/service/useFileRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +660 -68
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts +4 -0
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
- package/dist/service/useSharedDataRoutes.js +104 -0
- package/dist/service/useSharedDataRoutes.js.map +1 -0
- package/dist/service/useSharedFileRoutes.d.ts +4 -0
- package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
- package/dist/service/useSharedFileRoutes.js +121 -0
- package/dist/service/useSharedFileRoutes.js.map +1 -0
- package/dist/settings.d.ts +3 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +5 -8
- package/dist/settings.js.map +1 -1
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +4 -3
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +15 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +106 -20
- package/dist/themes.js.map +1 -1
- package/migration-rules/v1-to-v2.md +193 -0
- package/migration-rules/v2-to-v3.md +481 -0
- package/package.json +15 -11
- package/required-pages/builder/page.html +43 -0
- package/required-pages/builder/page.json +10 -0
- package/required-pages/pages/page.html +924 -0
- package/required-pages/pages/page.json +10 -0
- package/required-pages/settings/page.html +1753 -0
- package/required-pages/settings/page.json +10 -0
- package/required-pages/synthos_apis/page.html +846 -0
- package/required-pages/synthos_apis/page.json +10 -0
- package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
- package/required-pages/synthos_scripts/page.json +10 -0
- package/service-connectors/airtable/connector.json +27 -0
- package/service-connectors/alpha-vantage/connector.json +26 -0
- package/service-connectors/brave-search/connector.json +26 -0
- package/service-connectors/cloudinary/connector.json +27 -0
- package/service-connectors/deepl/connector.json +28 -0
- package/service-connectors/elevenlabs/connector.json +30 -0
- package/service-connectors/giphy/connector.json +27 -0
- package/service-connectors/github/connector.json +29 -0
- package/service-connectors/huggingface/connector.json +27 -0
- package/service-connectors/imgur/connector.json +29 -0
- package/service-connectors/instagram/connector.json +43 -0
- package/service-connectors/jira/connector.json +28 -0
- package/service-connectors/mapbox/connector.json +26 -0
- package/service-connectors/nasa/connector.json +27 -0
- package/service-connectors/newsapi/connector.json +27 -0
- package/service-connectors/notion/connector.json +28 -0
- package/service-connectors/open-exchange-rates/connector.json +27 -0
- package/service-connectors/openweathermap/connector.json +26 -0
- package/service-connectors/pexels/connector.json +27 -0
- package/service-connectors/resend/connector.json +29 -0
- package/service-connectors/rss2json/connector.json +27 -0
- package/service-connectors/sendgrid/connector.json +27 -0
- package/service-connectors/spoonacular/connector.json +28 -0
- package/service-connectors/stability-ai/connector.json +27 -0
- package/service-connectors/twilio/connector.json +28 -0
- package/service-connectors/unsplash/connector.json +27 -0
- package/service-connectors/wolfram-alpha/connector.json +26 -0
- package/service-connectors/youtube-data/connector.json +30 -0
- package/src/agents/a2a/a2aProvider.ts +110 -0
- package/src/agents/discovery.ts +74 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/openclaw/gatewayManager.ts +570 -0
- package/src/agents/openclaw/openclawProvider.ts +259 -0
- package/src/agents/openclaw/sshTunnelManager.ts +393 -0
- package/src/agents/types.ts +82 -0
- package/src/builders/anthropic.ts +283 -0
- package/src/builders/fireworksai.ts +59 -0
- package/src/builders/index.ts +33 -0
- package/src/builders/openai.ts +89 -0
- package/src/builders/types.ts +261 -0
- package/src/connectors/index.ts +3 -1
- package/src/connectors/registry.ts +40 -96
- package/src/connectors/types.ts +25 -0
- package/src/customizer/Customizer.ts +151 -0
- package/src/customizer/index.ts +5 -0
- package/src/files.ts +71 -0
- package/src/index.ts +2 -1
- package/src/init.ts +138 -97
- package/src/migrations.ts +148 -145
- package/src/models/anthropic.ts +119 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +143 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +110 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +97 -2
- package/src/models/utils.ts +16 -0
- package/src/pages.ts +176 -54
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +3 -1
- package/src/service/generateImage.ts +2 -2
- package/src/service/server.ts +39 -8
- package/src/service/transformPage.ts +605 -301
- package/src/service/useAgentRoutes.ts +428 -0
- package/src/service/useApiRoutes.ts +309 -45
- package/src/service/useConnectorRoutes.ts +21 -10
- package/src/service/useFileRoutes.ts +127 -0
- package/src/service/usePageRoutes.ts +736 -75
- package/src/service/useSharedDataRoutes.ts +106 -0
- package/src/service/useSharedFileRoutes.ts +126 -0
- package/src/settings.ts +8 -10
- package/src/synthos-cli.ts +4 -3
- package/src/themes.ts +103 -20
- package/static-files/favicon.svg +12 -0
- package/static-files/fluentlm-instructions.llmd +868 -0
- package/static-files/fluentlm-instructions.md +1595 -0
- package/static-files/fluentlm.css +4844 -0
- package/static-files/fluentlm.js +3602 -0
- package/static-files/fluentlm.min.css +1 -0
- package/static-files/fluentlm.min.js +1 -0
- package/static-files/helpers.v3.js +304 -0
- package/static-files/page.v3.js +1290 -0
- package/static-files/recommended-frameworks.llmd +81 -0
- package/static-files/recommended-frameworks.md +137 -0
- package/static-files/retro-game.js +877 -0
- package/static-files/shell.css +797 -0
- package/static-files/theme-dark.css +169 -0
- package/static-files/theme-light.css +169 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/builders.spec.ts +139 -0
- package/tests/chainOfThought.spec.ts +108 -0
- package/tests/ensureScripts.spec.ts +82 -0
- package/tests/files.spec.ts +233 -0
- package/tests/fireworksai.spec.ts +92 -0
- package/tests/logCompletePrompt.spec.ts +74 -0
- package/tests/migrations.spec.ts +79 -1
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +226 -1
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +456 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.html +0 -40
- package/default-pages/app_builder.json +0 -1
- package/default-pages/json_tools.json +0 -1
- package/default-pages/my_notes.html +0 -33
- package/default-pages/neon_asteroids.html +0 -77
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/solar_tutorial.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/dist/connectors/index.d.ts +0 -3
- package/dist/connectors/types.d.ts +0 -61
- package/dist/index.d.ts +0 -7
- package/dist/migrations.d.ts +0 -11
- package/dist/models/providers.d.ts +0 -7
- package/dist/scripts.d.ts +0 -14
- package/dist/service/createCompletePrompt.d.ts +0 -5
- package/dist/service/debugLog.d.ts +0 -11
- package/dist/service/generateImage.d.ts +0 -32
- package/dist/service/index.d.ts +0 -8
- package/dist/service/modelInstructions.d.ts +0 -7
- package/dist/service/requiresSettings.d.ts +0 -3
- package/dist/service/server.d.ts +0 -4
- package/dist/service/useApiRoutes.d.ts +0 -4
- package/dist/service/useConnectorRoutes.d.ts +0 -4
- package/dist/service/useDataRoutes.d.ts +0 -4
- package/dist/service/usePageRoutes.d.ts +0 -5
- package/dist/synthos-cli.d.ts +0 -2
- package/images/home.png +0 -0
- package/images/page-management.png +0 -0
- package/images/settings.png +0 -0
- package/images/synthos-square.png +0 -0
- package/page-scripts/helpers-v2.js +0 -121
- package/page-scripts/page-v2.js +0 -615
- package/required-pages/builder.html +0 -74
- package/required-pages/builder.json +0 -1
- package/required-pages/pages.html +0 -196
- package/required-pages/pages.json +0 -1
- package/required-pages/settings.html +0 -841
- package/required-pages/settings.json +0 -1
- package/required-pages/synthos_apis.html +0 -272
- package/required-pages/synthos_apis.json +0 -1
- package/required-pages/synthos_scripts.json +0 -1
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head>
|
|
2
|
+
<meta charset="UTF-8">
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4
|
+
<title>SynthOS - ElevenLabs Voice Studio</title>
|
|
5
|
+
<script id="theme-info" src="/api/theme-info.js" data-locked="true"></script>
|
|
6
|
+
<link id="theme-css" rel="stylesheet" href="/api/theme.css" data-locked="true">
|
|
7
|
+
<style id="voice-studio-layout">
|
|
8
|
+
.vs-app { display: flex; height: 100%; width: 100%; overflow: hidden; }
|
|
9
|
+
.vs-sidebar { width: 280px; min-width: 280px; background: var(--defaultStateBackground); border-right: 2px solid var(--neutralLight); display: flex; flex-direction: column; }
|
|
10
|
+
.vs-sidebar-header { padding: 16px; border-bottom: 1px solid var(--neutralLight); display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; }
|
|
11
|
+
.vs-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
12
|
+
.vs-main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; background: var(--bodyBackground); }
|
|
13
|
+
.vs-instructions { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px; }
|
|
14
|
+
.vs-detail { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
15
|
+
.vs-detail-header { display: flex; align-items: center; gap: 12px; padding: 20px 20px 12px; flex-shrink: 0; border-bottom: 1px solid var(--neutralLight); }
|
|
16
|
+
.vs-detail-body { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 16px; }
|
|
17
|
+
.vs-detail-footer { flex-shrink: 0; padding: 12px 20px; border-top: 1px solid var(--neutralLight); }
|
|
18
|
+
.vs-detail-header input { font-size: 22px; font-weight: 600; flex: 1; }
|
|
19
|
+
.vs-section { padding: 16px; background: var(--defaultStateBackground); border-radius: 8px; border: 1px solid var(--neutralLight); }
|
|
20
|
+
.vs-section h3 { color: var(--themePrimary); font-weight: 600; margin: 0 0 12px; font-size: 15px; }
|
|
21
|
+
.slider-row { margin-bottom: 12px; }
|
|
22
|
+
.slider-row label { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 13px; color: var(--bodyText); }
|
|
23
|
+
.slider-row label span { color: var(--themePrimary); font-weight: 600; }
|
|
24
|
+
.slider-row input[type="range"] { width: 100%; height: 6px; border-radius: 3px; background: var(--neutralLight); -webkit-appearance: none; appearance: none; cursor: pointer; }
|
|
25
|
+
.slider-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--themePrimary); cursor: pointer; }
|
|
26
|
+
.slider-row input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--themePrimary); cursor: pointer; border: none; }
|
|
27
|
+
.filter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; margin-bottom: 10px; }
|
|
28
|
+
.filter-grid label { font-size: 11px; color: var(--bodySubtext); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; display: block; }
|
|
29
|
+
.filter-grid select, .filter-grid input { width: 100%; padding: 6px 8px; border: 1px solid var(--neutralLight); border-radius: 6px; background: var(--inputBackground); color: var(--bodyText); font-size: 12px; }
|
|
30
|
+
.selected-voice-field { display: flex; align-items: center; gap: 10px; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; border: 1px solid var(--themePrimary); background: var(--themeLighter); }
|
|
31
|
+
.selected-voice-field-label { font-size: 11px; color: var(--bodySubtext); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
32
|
+
.selected-voice-field-name { font-weight: 600; font-size: 14px; flex: 1; }
|
|
33
|
+
.selected-voice-field--empty { border-color: var(--neutralLight); background: var(--inputBackground); }
|
|
34
|
+
.selected-voice-field--empty .selected-voice-field-name { color: var(--bodySubtext); font-weight: 400; }
|
|
35
|
+
.voice-card-list { display: flex; flex-direction: column; gap: 4px; }
|
|
36
|
+
.voice-card { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; transition: background 0.15s, border-color 0.15s; }
|
|
37
|
+
.voice-card:hover { background: var(--neutralLight); }
|
|
38
|
+
.voice-card--selected { border-color: var(--themePrimary); background: var(--themeLighter); box-shadow: inset 0 0 0 1px var(--themePrimary); }
|
|
39
|
+
.voice-card-select { flex-shrink: 0; padding: 4px 10px; border-radius: 4px; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; font-size: 11px; color: var(--bodyText); transition: background 0.15s; }
|
|
40
|
+
.voice-card-select:hover { background: var(--neutralLight); }
|
|
41
|
+
.voice-card--selected .voice-card-select { background: var(--themePrimary); color: #fff; border-color: var(--themePrimary); }
|
|
42
|
+
.voice-card-info { flex: 1; min-width: 0; }
|
|
43
|
+
.voice-card-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
44
|
+
.voice-card-meta { font-size: 11px; color: var(--bodySubtext); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
45
|
+
.voice-card-badge { display: inline-block; font-size: 10px; padding: 1px 6px; border-radius: 3px; background: var(--neutralLight); color: var(--bodySubtext); margin-left: 6px; vertical-align: middle; }
|
|
46
|
+
.voice-card-preview { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--neutralLight); background: var(--inputBackground); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--themePrimary); font-size: 14px; transition: background 0.15s; }
|
|
47
|
+
.voice-card-preview:hover { background: var(--neutralLight); }
|
|
48
|
+
.voice-card-list-empty { padding: 20px; text-align: center; font-size: 12px; color: var(--bodySubtext); }
|
|
49
|
+
.pagination-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
|
50
|
+
.pagination-info { font-size: 12px; color: var(--bodySubtext); }
|
|
51
|
+
.char-count { text-align: right; font-size: 12px; color: var(--bodySubtext); margin-top: 4px; }
|
|
52
|
+
.waveform-container { height: 50px; display: flex; align-items: center; justify-content: center; gap: 3px; margin-bottom: 10px; }
|
|
53
|
+
.waveform-bar { width: 4px; background: var(--themePrimary); border-radius: 2px; transition: height 0.1s ease; }
|
|
54
|
+
.audio-row { text-align: center; }
|
|
55
|
+
.audio-row audio { width: 100%; margin-bottom: 10px; }
|
|
56
|
+
.vs-actions { display: flex; gap: 12px; justify-content: flex-end; }
|
|
57
|
+
.save-toast { opacity: 0; transform: translateY(4px); transition: opacity .3s, transform .3s; pointer-events: none; }
|
|
58
|
+
.save-toast.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
|
59
|
+
</style>
|
|
60
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js"></script>
|
|
61
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="shell-toolbar" data-locked="true">
|
|
65
|
+
<button class="shell-toolbar-btn" id="builderToggle" aria-label="Page Builder" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M7 18.5H6.2c-1.77 0-3.2-1.43-3.2-3.2V7.7C3 5.93 4.43 4.5 6.2 4.5h11.6c1.77 0 3.2 1.43 3.2 3.2v7.6c0 1.77-1.43 3.2-3.2 3.2H12l-4.2 3.2c-.5.38-1.2.02-1.2-.6V18.5Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="8.5" cy="11.5" r="1" fill="currentColor"/><circle cx="12" cy="11.5" r="1" fill="currentColor"/><circle cx="15.5" cy="11.5" r="1" fill="currentColor"/></svg></button>
|
|
66
|
+
<button class="shell-toolbar-btn" id="pagesBtn" aria-label="View All Pages" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"><rect x="3" y="3" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M6 7.5h5M6 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="18" y="3" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M21 7.5h5M21 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="3" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M6 22.5h5M6 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="18" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M21 22.5h5M21 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
|
|
67
|
+
<button class="shell-toolbar-btn" id="saveBtn" aria-label="Save Page" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M17 21v-8H7v8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M7 3v5h8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button>
|
|
68
|
+
<div class="shell-toolbar-spacer" data-locked="true"></div>
|
|
69
|
+
<button class="shell-toolbar-btn" id="settingsBtn" aria-label="Settings" data-locked="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" stroke-width="1.8"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="chat-panel" data-locked="true">
|
|
72
|
+
<div class="chat-header" data-locked="true"><span>Page Builder</span><button class="chat-header-close" id="builderClose" aria-label="Close builder" data-locked="true">×</button></div>
|
|
73
|
+
<div class="chat-messages" id="chatMessages" data-locked="true">
|
|
74
|
+
<div class="chat-message">
|
|
75
|
+
<p><strong>SynthOS:</strong> Welcome to the ElevenLabs Voice Studio! Define voice profiles here and they'll be available to all your pages. Make sure the ElevenLabs connector is configured in Settings > Connectors.</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div> <form action="/" method="POST" id="chatForm" data-locked="true">
|
|
78
|
+
<textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..." data-locked="true"></textarea>
|
|
79
|
+
</form>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="viewer-panel" id="viewerPanel" style="justify-content: flex-start; align-items: stretch;">
|
|
82
|
+
<div class="vs-app">
|
|
83
|
+
<!-- Sidebar: voice list -->
|
|
84
|
+
<div class="vs-sidebar">
|
|
85
|
+
<div class="vs-sidebar-header">
|
|
86
|
+
<span class="flm-text flm-text--large flm-text--bold">Voices</span>
|
|
87
|
+
<button class="flm-button flm-button--primary" id="addVoiceBtn" data-icon="Add">New Voice</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="vs-list flm-list flm-list--bordered" id="voicesList"></div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Main: detail or instructions -->
|
|
93
|
+
<div class="vs-main">
|
|
94
|
+
<!-- Connector status -->
|
|
95
|
+
<div id="connectorStatus" style="padding:12px 20px 0;"></div>
|
|
96
|
+
|
|
97
|
+
<!-- Empty state -->
|
|
98
|
+
<div class="vs-instructions" id="vsInstructions">
|
|
99
|
+
<i class="flm-icon flm-icon--large" data-icon="Microphone" style="font-size:64px;opacity:0.6;margin-bottom:20px;"></i>
|
|
100
|
+
<span class="flm-text flm-text--xLarge flm-text--bold flm-text--block" style="margin-bottom:12px;">Voice Studio</span>
|
|
101
|
+
<span class="flm-text flm-text--secondary flm-text--block" style="max-width:320px;line-height:1.5;">Select a voice from the sidebar to view its settings and test it, or click "+ New Voice" to create a profile.</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Detail editor -->
|
|
105
|
+
<div class="vs-detail" id="vsDetail" style="display:none;">
|
|
106
|
+
<!-- Name -->
|
|
107
|
+
<div class="vs-detail-header">
|
|
108
|
+
<div class="flm-textfield" style="flex:1;">
|
|
109
|
+
<input class="flm-textfield-input" id="profileNameInput" placeholder="Voice profile name...">
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Scrollable body -->
|
|
114
|
+
<div class="vs-detail-body">
|
|
115
|
+
<!-- Model -->
|
|
116
|
+
<div class="vs-section">
|
|
117
|
+
<div class="flm-textfield">
|
|
118
|
+
<label class="flm-label" for="modelSelect">Model</label>
|
|
119
|
+
<select class="flm-textfield-input" id="modelSelect">
|
|
120
|
+
<option value="eleven_turbo_v2_5">Eleven Turbo v2.5 (Fastest)</option>
|
|
121
|
+
<option value="eleven_multilingual_v2">Eleven Multilingual v2</option>
|
|
122
|
+
<option value="eleven_turbo_v2">Eleven Turbo v2</option>
|
|
123
|
+
<option value="eleven_monolingual_v1">Eleven Monolingual v1</option>
|
|
124
|
+
</select>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Voice selection -->
|
|
129
|
+
<div class="vs-section">
|
|
130
|
+
<h3>ElevenLabs Voice</h3>
|
|
131
|
+
<div class="selected-voice-field selected-voice-field--empty" id="selectedVoiceField">
|
|
132
|
+
<div style="flex:1;">
|
|
133
|
+
<div class="selected-voice-field-label">Selected Voice</div>
|
|
134
|
+
<div class="selected-voice-field-name" id="selectedVoiceName">No voice selected</div>
|
|
135
|
+
</div>
|
|
136
|
+
<button class="flm-button" id="changeVoiceBtn">Change Voice</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Voice Settings -->
|
|
141
|
+
<div class="vs-section">
|
|
142
|
+
<h3>Voice Settings</h3>
|
|
143
|
+
<div class="slider-row"><label>Stability <span id="stabilityValue">50%</span></label><input type="range" id="stability" min="0" max="100" value="50"></div>
|
|
144
|
+
<div class="slider-row"><label>Similarity <span id="similarityValue">75%</span></label><input type="range" id="similarity" min="0" max="100" value="75"></div>
|
|
145
|
+
<div class="slider-row"><label>Style <span id="styleValue">0%</span></label><input type="range" id="style" min="0" max="100" value="0"></div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Test TTS -->
|
|
149
|
+
<div class="vs-section">
|
|
150
|
+
<h3>Test Voice</h3>
|
|
151
|
+
<div class="flm-textfield" style="margin-bottom:12px;">
|
|
152
|
+
<textarea class="flm-textfield-input" id="textInput" rows="3" placeholder="Enter text to test...">Hello! This is a test of the voice profile.</textarea>
|
|
153
|
+
</div>
|
|
154
|
+
<p class="char-count"><span id="charCount">0</span> characters</p>
|
|
155
|
+
<div id="statusMessage" class="flm-messagebar" style="display:none;margin-bottom:12px;"></div>
|
|
156
|
+
<button class="flm-button flm-button--primary" id="generateBtn" style="width:100%;padding:12px;">Generate Speech</button>
|
|
157
|
+
<div id="audioSection" style="display:none;margin-top:12px;">
|
|
158
|
+
<div class="waveform-container" id="waveform"></div>
|
|
159
|
+
<div class="audio-row">
|
|
160
|
+
<audio id="audioPlayer" controls style="width:100%;"></audio>
|
|
161
|
+
<div style="margin-top:8px;">
|
|
162
|
+
<button class="flm-button" id="downloadBtn">Download</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Footer: Save / Delete (always visible) -->
|
|
170
|
+
<div class="vs-detail-footer">
|
|
171
|
+
<div id="saveToast" class="flm-messagebar flm-messagebar--success save-toast" style="margin-bottom:8px;"></div>
|
|
172
|
+
<div class="vs-actions">
|
|
173
|
+
<button class="flm-button" id="deleteVoiceBtn" data-icon="Delete" style="color:var(--errorText);">Delete</button>
|
|
174
|
+
<button class="flm-button" id="saveVoiceBtn" data-icon="Save">Save</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Voice picker dialog -->
|
|
182
|
+
<div id="voicePickerDialog" class="flm-dialog-overlay" data-light-dismiss>
|
|
183
|
+
<div class="flm-dialog" style="max-width:600px;width:95%;">
|
|
184
|
+
<div class="flm-dialog-header">
|
|
185
|
+
<h2 class="flm-dialog-title">Select Voice</h2>
|
|
186
|
+
<button class="flm-dialog-close" id="voicePickerClose">×</button>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="flm-dialog-body">
|
|
189
|
+
<div class="filter-grid">
|
|
190
|
+
<div><label>Search</label><input type="text" id="searchFilter" placeholder="Search..."></div>
|
|
191
|
+
<div><label>Type</label><select id="voiceTypeFilter"><option value="">All</option><option value="personal">Personal</option><option value="community">Community</option><option value="default">Default</option><option value="workspace">Workspace</option></select></div>
|
|
192
|
+
<div><label>Category</label><select id="categoryFilter"><option value="">All</option><option value="premade">Premade</option><option value="cloned">Cloned</option><option value="generated">Generated</option><option value="professional">Professional</option></select></div>
|
|
193
|
+
<div><label>Sort</label><select id="sortFilter"><option value="">Default</option><option value="name">Name</option><option value="created_at_unix">Created</option></select></div>
|
|
194
|
+
<div><label>Direction</label><select id="sortDirectionFilter"><option value="asc">Asc</option><option value="desc">Desc</option></select></div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="voice-card-list" id="voiceCardList">
|
|
197
|
+
<div class="voice-card-list-empty">Loading voices...</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="pagination-info" id="voiceCount"></div>
|
|
200
|
+
<div class="pagination-row" id="paginationControls" style="display:none;">
|
|
201
|
+
<button class="flm-button flm-button--subtle" id="prevPageBtn" disabled>« Prev</button>
|
|
202
|
+
<span class="pagination-info" id="pageInfo">Page 1</span>
|
|
203
|
+
<button class="flm-button flm-button--subtle" id="nextPageBtn" disabled>Next »</button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="flm-dialog-footer">
|
|
207
|
+
<button class="flm-button" id="voicePickerCancel">Cancel</button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Delete confirm dialog -->
|
|
213
|
+
<div id="deleteConfirmDialog" class="flm-dialog-overlay" data-light-dismiss>
|
|
214
|
+
<div class="flm-dialog" style="max-width:400px;width:90%;">
|
|
215
|
+
<div class="flm-dialog-header"><h2 class="flm-dialog-title">Delete Voice</h2></div>
|
|
216
|
+
<div class="flm-dialog-body"><p class="flm-text">Are you sure you want to delete this voice profile? This cannot be undone.</p></div>
|
|
217
|
+
<div class="flm-dialog-footer">
|
|
218
|
+
<div style="flex:1;"></div>
|
|
219
|
+
<button class="flm-button" id="deleteConfirmCancel">Cancel</button>
|
|
220
|
+
<button class="flm-button" id="deleteConfirmOk" style="color:var(--errorText);">Delete</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div id="loadingOverlay" class="loading-overlay"><div class="spinner"></div></div>
|
|
226
|
+
</div>
|
|
227
|
+
<div id="instructions" style="display:none;" data-locked="true"></div>
|
|
228
|
+
<div id="thoughts" style="display:none;" data-locked="true"></div>
|
|
229
|
+
<script id="voice-studio-logic">
|
|
230
|
+
// ============ STATE ============
|
|
231
|
+
const TABLE = 'elevenlabs_voices';
|
|
232
|
+
const DEFAULT_VOICE = {
|
|
233
|
+
id: 'default',
|
|
234
|
+
name: 'River',
|
|
235
|
+
model: 'eleven_turbo_v2_5',
|
|
236
|
+
voiceName: 'River',
|
|
237
|
+
voiceId: '',
|
|
238
|
+
stability: 50,
|
|
239
|
+
similarity: 75,
|
|
240
|
+
style: 0
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let profiles = [];
|
|
244
|
+
let currentId = null;
|
|
245
|
+
let currentAudioBlob = null;
|
|
246
|
+
let voicesData = [];
|
|
247
|
+
let currentPageToken = null;
|
|
248
|
+
let pageTokenHistory = [null];
|
|
249
|
+
let currentPageIndex = 0;
|
|
250
|
+
let hasMore = false;
|
|
251
|
+
let totalVoiceCount = 0;
|
|
252
|
+
let isDirty = false;
|
|
253
|
+
|
|
254
|
+
let selectedVoiceId = '';
|
|
255
|
+
let previewAudio = null;
|
|
256
|
+
|
|
257
|
+
const $ = id => document.getElementById(id);
|
|
258
|
+
const profileNameInput = $('profileNameInput');
|
|
259
|
+
const modelSelect = $('modelSelect');
|
|
260
|
+
const voiceCardList = $('voiceCardList');
|
|
261
|
+
const stabilitySlider = $('stability');
|
|
262
|
+
const similaritySlider = $('similarity');
|
|
263
|
+
const styleSlider = $('style');
|
|
264
|
+
const stabilityVal = $('stabilityValue');
|
|
265
|
+
const similarityVal = $('similarityValue');
|
|
266
|
+
const styleVal = $('styleValue');
|
|
267
|
+
const textInput = $('textInput');
|
|
268
|
+
const charCount = $('charCount');
|
|
269
|
+
const generateBtn = $('generateBtn');
|
|
270
|
+
const statusMsg = $('statusMessage');
|
|
271
|
+
const audioSection = $('audioSection');
|
|
272
|
+
const audioPlayer = $('audioPlayer');
|
|
273
|
+
const waveformContainer = $('waveform');
|
|
274
|
+
|
|
275
|
+
// ============ INIT ============
|
|
276
|
+
window.addEventListener('load', async () => {
|
|
277
|
+
updateCharCount();
|
|
278
|
+
await checkConnector();
|
|
279
|
+
await loadProfiles();
|
|
280
|
+
await loadVoices();
|
|
281
|
+
// Auto-select default profile on first load
|
|
282
|
+
if (profiles.length > 0) {
|
|
283
|
+
await selectProfile(profiles[0].id);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ============ CONNECTOR CHECK ============
|
|
288
|
+
async function checkConnector() {
|
|
289
|
+
try {
|
|
290
|
+
const connectors = await synthos.connectors.list({ id: 'elevenlabs' });
|
|
291
|
+
const el = Array.isArray(connectors) ? connectors.find(c => c.id === 'elevenlabs') : null;
|
|
292
|
+
if (!el || !el.configured) {
|
|
293
|
+
$('connectorStatus').innerHTML = '<div class="flm-messagebar flm-messagebar--warning">ElevenLabs connector not configured. Go to <a href="/settings" class="flm-link">Settings</a> > Connectors to set up your API key.</div>';
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
$('connectorStatus').innerHTML = '<div class="flm-messagebar flm-messagebar--warning">Could not check connector status.</div>';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============ PROFILES CRUD ============
|
|
301
|
+
async function loadProfiles() {
|
|
302
|
+
try {
|
|
303
|
+
const rows = await synthos.shared.data.list(TABLE);
|
|
304
|
+
profiles = Array.isArray(rows) ? rows : rows?.items || [];
|
|
305
|
+
} catch (e) {
|
|
306
|
+
profiles = [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Ensure default "River" profile exists
|
|
310
|
+
if (!profiles.find(p => p.id === 'default')) {
|
|
311
|
+
await synthos.shared.data.save(TABLE, { ...DEFAULT_VOICE });
|
|
312
|
+
profiles.unshift({ ...DEFAULT_VOICE });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
renderList();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderList() {
|
|
319
|
+
const list = $('voicesList');
|
|
320
|
+
if (profiles.length === 0) {
|
|
321
|
+
list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--bodySubtext);font-size:13px;">No voices yet.</div>';
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
list.innerHTML = '';
|
|
325
|
+
profiles.forEach(p => {
|
|
326
|
+
const item = document.createElement('div');
|
|
327
|
+
item.className = 'flm-list-item' + (currentId === p.id ? ' flm-list-item--selected' : '');
|
|
328
|
+
item.style.cssText = 'cursor:pointer;padding:12px 14px;';
|
|
329
|
+
item.innerHTML = '<div class="flm-list-item-content">' +
|
|
330
|
+
'<span class="flm-list-item-primary flm-text--semibold flm-text--nowrap">' + escHtml(p.name || 'Untitled') + '</span>' +
|
|
331
|
+
'<span class="flm-list-item-secondary flm-text--small flm-text--secondary">' + escHtml(p.voiceName || p.voiceId || 'No voice') + ' • ' + escHtml(p.model || '') + '</span>' +
|
|
332
|
+
'</div>';
|
|
333
|
+
item.onclick = () => selectProfile(p.id);
|
|
334
|
+
list.appendChild(item);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function selectProfile(id) {
|
|
339
|
+
currentId = id;
|
|
340
|
+
const p = profiles.find(x => x.id === id);
|
|
341
|
+
if (!p) return;
|
|
342
|
+
|
|
343
|
+
$('vsInstructions').style.display = 'none';
|
|
344
|
+
$('vsDetail').style.display = '';
|
|
345
|
+
|
|
346
|
+
profileNameInput.value = p.name || '';
|
|
347
|
+
modelSelect.value = p.model || 'eleven_turbo_v2_5';
|
|
348
|
+
stabilitySlider.value = p.stability ?? 50; stabilityVal.textContent = stabilitySlider.value + '%';
|
|
349
|
+
similaritySlider.value = p.similarity ?? 75; similarityVal.textContent = similaritySlider.value + '%';
|
|
350
|
+
styleSlider.value = p.style ?? 0; styleVal.textContent = styleSlider.value + '%';
|
|
351
|
+
|
|
352
|
+
// Reset test area
|
|
353
|
+
audioSection.style.display = 'none';
|
|
354
|
+
currentAudioBlob = null;
|
|
355
|
+
hideStatus();
|
|
356
|
+
isDirty = false;
|
|
357
|
+
updateSaveBtn();
|
|
358
|
+
renderList();
|
|
359
|
+
|
|
360
|
+
// Reset voice selection to this profile's saved voice
|
|
361
|
+
selectedVoiceId = p.voiceId || '';
|
|
362
|
+
updateSelectedVoiceField(p.voiceName || '');
|
|
363
|
+
renderVoiceCards();
|
|
364
|
+
|
|
365
|
+
// Resolve voice (by ID or name fallback via API)
|
|
366
|
+
await resolveVoiceSelection(p);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function resolveVoiceSelection(p) {
|
|
370
|
+
// Try by saved voice ID first
|
|
371
|
+
if (p.voiceId) {
|
|
372
|
+
const found = voicesData.find(v => v.voice_id === p.voiceId);
|
|
373
|
+
if (found) {
|
|
374
|
+
selectVoiceCard(p.voiceId, true);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Fallback: match by voice name (e.g. default "River" with no saved ID)
|
|
379
|
+
// ElevenLabs v2 API returns names like "River - Expressive, Gentle" so match the prefix before " - "
|
|
380
|
+
if (p.voiceName) {
|
|
381
|
+
const target = p.voiceName.toLowerCase();
|
|
382
|
+
const matchName = (v) => {
|
|
383
|
+
const name = v.name.toLowerCase();
|
|
384
|
+
return name === target || name.split(' - ')[0] === target;
|
|
385
|
+
};
|
|
386
|
+
// Check loaded voices first
|
|
387
|
+
let match = voicesData.find(matchName);
|
|
388
|
+
// Always try API search if not found locally
|
|
389
|
+
if (!match) {
|
|
390
|
+
try {
|
|
391
|
+
const data = await synthos.connectors.call('elevenlabs', 'GET',
|
|
392
|
+
'/v2/voices?search=' + encodeURIComponent(p.voiceName) + '&page_size=10');
|
|
393
|
+
const voices = data.voices || [];
|
|
394
|
+
match = voices.find(matchName);
|
|
395
|
+
if (match) {
|
|
396
|
+
// Add to local data so it's selectable as a card
|
|
397
|
+
if (!voicesData.find(v => v.voice_id === match.voice_id)) {
|
|
398
|
+
voicesData.push(match);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error('[VoiceStudio] Voice search failed:', e);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (match) {
|
|
406
|
+
selectedVoiceId = match.voice_id;
|
|
407
|
+
updateSelectedVoiceField(match.name);
|
|
408
|
+
renderVoiceCards();
|
|
409
|
+
// Persist the resolved ID so future loads are instant
|
|
410
|
+
p.voiceId = match.voice_id;
|
|
411
|
+
const idx = profiles.findIndex(x => x.id === p.id);
|
|
412
|
+
if (idx >= 0) profiles[idx] = p;
|
|
413
|
+
synthos.shared.data.save(TABLE, p);
|
|
414
|
+
} else {
|
|
415
|
+
console.warn('[VoiceStudio] Could not resolve voice "' + p.voiceName + '".',
|
|
416
|
+
'voicesData has', voicesData.length, 'voices.',
|
|
417
|
+
voicesData.length > 0 ? 'First 5: ' + voicesData.slice(0, 5).map(v => v.name).join(', ') : '(empty)');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function showInstructions() {
|
|
423
|
+
currentId = null;
|
|
424
|
+
selectedVoiceId = '';
|
|
425
|
+
$('vsInstructions').style.display = '';
|
|
426
|
+
$('vsDetail').style.display = 'none';
|
|
427
|
+
renderList();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Dirty tracking
|
|
431
|
+
function markDirty() { isDirty = true; updateSaveBtn(); }
|
|
432
|
+
function updateSaveBtn() {
|
|
433
|
+
const btn = $('saveVoiceBtn');
|
|
434
|
+
if (isDirty) btn.classList.add('flm-button--primary');
|
|
435
|
+
else btn.classList.remove('flm-button--primary');
|
|
436
|
+
}
|
|
437
|
+
function showSaveToast() {
|
|
438
|
+
const t = $('saveToast');
|
|
439
|
+
t.textContent = 'Voice saved';
|
|
440
|
+
t.classList.add('show');
|
|
441
|
+
setTimeout(() => t.classList.remove('show'), 2500);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
profileNameInput.addEventListener('input', markDirty);
|
|
445
|
+
modelSelect.addEventListener('change', markDirty);
|
|
446
|
+
stabilitySlider.addEventListener('input', () => { stabilityVal.textContent = stabilitySlider.value + '%'; markDirty(); });
|
|
447
|
+
similaritySlider.addEventListener('input', () => { similarityVal.textContent = similaritySlider.value + '%'; markDirty(); });
|
|
448
|
+
styleSlider.addEventListener('input', () => { styleVal.textContent = styleSlider.value + '%'; markDirty(); });
|
|
449
|
+
|
|
450
|
+
// ============ VOICE CARDS ============
|
|
451
|
+
// ElevenLabs v2 returns "River - Expressive, Gentle"; extract just "River"
|
|
452
|
+
function shortVoiceName(name) { return name ? name.split(' - ')[0] : ''; }
|
|
453
|
+
|
|
454
|
+
function updateSelectedVoiceField(name) {
|
|
455
|
+
const field = $('selectedVoiceField');
|
|
456
|
+
const nameEl = $('selectedVoiceName');
|
|
457
|
+
if (name) {
|
|
458
|
+
field.classList.remove('selected-voice-field--empty');
|
|
459
|
+
nameEl.textContent = name;
|
|
460
|
+
} else {
|
|
461
|
+
field.classList.add('selected-voice-field--empty');
|
|
462
|
+
nameEl.textContent = 'No voice selected';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function selectVoiceCard(voiceId, silent) {
|
|
467
|
+
selectedVoiceId = voiceId;
|
|
468
|
+
const voice = voicesData.find(v => v.voice_id === voiceId);
|
|
469
|
+
const short = shortVoiceName(voice?.name || '');
|
|
470
|
+
updateSelectedVoiceField(short);
|
|
471
|
+
if (!silent) {
|
|
472
|
+
// Auto-fill profile name if empty
|
|
473
|
+
if (!profileNameInput.value.trim()) {
|
|
474
|
+
profileNameInput.value = short;
|
|
475
|
+
}
|
|
476
|
+
markDirty();
|
|
477
|
+
closeVoicePickerDialog();
|
|
478
|
+
}
|
|
479
|
+
renderVoiceCards();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function renderVoiceCards() {
|
|
483
|
+
voiceCardList.innerHTML = '';
|
|
484
|
+
if (voicesData.length === 0) {
|
|
485
|
+
voiceCardList.innerHTML = '<div class="voice-card-list-empty">No voices found</div>';
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Show selected voice first
|
|
489
|
+
const sorted = [...voicesData].sort((a, b) => {
|
|
490
|
+
if (a.voice_id === selectedVoiceId) return -1;
|
|
491
|
+
if (b.voice_id === selectedVoiceId) return 1;
|
|
492
|
+
return 0;
|
|
493
|
+
});
|
|
494
|
+
sorted.forEach(v => {
|
|
495
|
+
const card = document.createElement('div');
|
|
496
|
+
card.className = 'voice-card' + (selectedVoiceId === v.voice_id ? ' voice-card--selected' : '');
|
|
497
|
+
card.dataset.voiceId = v.voice_id;
|
|
498
|
+
|
|
499
|
+
const info = document.createElement('div');
|
|
500
|
+
info.className = 'voice-card-info';
|
|
501
|
+
|
|
502
|
+
const nameRow = document.createElement('div');
|
|
503
|
+
nameRow.className = 'voice-card-name';
|
|
504
|
+
nameRow.textContent = v.name;
|
|
505
|
+
if (v.category) {
|
|
506
|
+
const badge = document.createElement('span');
|
|
507
|
+
badge.className = 'voice-card-badge';
|
|
508
|
+
badge.textContent = v.category;
|
|
509
|
+
nameRow.appendChild(badge);
|
|
510
|
+
}
|
|
511
|
+
info.appendChild(nameRow);
|
|
512
|
+
|
|
513
|
+
const lbls = v.labels || {};
|
|
514
|
+
const metaText = Object.values(lbls).filter(Boolean).join(', ') || v.description || '';
|
|
515
|
+
if (metaText) {
|
|
516
|
+
const meta = document.createElement('div');
|
|
517
|
+
meta.className = 'voice-card-meta';
|
|
518
|
+
meta.textContent = metaText;
|
|
519
|
+
info.appendChild(meta);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
card.appendChild(info);
|
|
523
|
+
|
|
524
|
+
// Preview button
|
|
525
|
+
if (v.preview_url) {
|
|
526
|
+
const prevBtn = document.createElement('button');
|
|
527
|
+
prevBtn.className = 'voice-card-preview';
|
|
528
|
+
prevBtn.innerHTML = '▶';
|
|
529
|
+
prevBtn.title = 'Preview voice';
|
|
530
|
+
prevBtn.addEventListener('click', (e) => {
|
|
531
|
+
e.stopPropagation();
|
|
532
|
+
if (previewAudio) { previewAudio.pause(); previewAudio = null; }
|
|
533
|
+
previewAudio = new Audio(v.preview_url);
|
|
534
|
+
previewAudio.play();
|
|
535
|
+
});
|
|
536
|
+
card.appendChild(prevBtn);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Select button
|
|
540
|
+
const isSelected = selectedVoiceId === v.voice_id;
|
|
541
|
+
const selBtn = document.createElement('button');
|
|
542
|
+
selBtn.className = 'voice-card-select';
|
|
543
|
+
selBtn.textContent = isSelected ? 'Selected' : 'Select';
|
|
544
|
+
selBtn.addEventListener('click', (e) => {
|
|
545
|
+
e.stopPropagation();
|
|
546
|
+
selectVoiceCard(v.voice_id);
|
|
547
|
+
});
|
|
548
|
+
card.appendChild(selBtn);
|
|
549
|
+
|
|
550
|
+
card.addEventListener('click', () => {
|
|
551
|
+
selectVoiceCard(v.voice_id);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
voiceCardList.appendChild(card);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ============ ADD / SAVE / DELETE ============
|
|
559
|
+
$('addVoiceBtn').addEventListener('click', async () => {
|
|
560
|
+
const newId = 'voice_' + Date.now();
|
|
561
|
+
const newProfile = {
|
|
562
|
+
id: newId,
|
|
563
|
+
name: '',
|
|
564
|
+
model: 'eleven_turbo_v2_5',
|
|
565
|
+
voiceName: '',
|
|
566
|
+
voiceId: '',
|
|
567
|
+
stability: 50,
|
|
568
|
+
similarity: 75,
|
|
569
|
+
style: 0
|
|
570
|
+
};
|
|
571
|
+
await synthos.shared.data.save(TABLE, newProfile);
|
|
572
|
+
profiles.push(newProfile);
|
|
573
|
+
selectProfile(newId);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
$('saveVoiceBtn').addEventListener('click', async () => {
|
|
577
|
+
if (!currentId) return;
|
|
578
|
+
const selectedVoice = voicesData.find(v => v.voice_id === selectedVoiceId);
|
|
579
|
+
const data = {
|
|
580
|
+
id: currentId,
|
|
581
|
+
name: profileNameInput.value.trim() || 'Untitled',
|
|
582
|
+
model: modelSelect.value,
|
|
583
|
+
voiceId: selectedVoiceId,
|
|
584
|
+
voiceName: shortVoiceName(selectedVoice?.name || ''),
|
|
585
|
+
stability: parseInt(stabilitySlider.value),
|
|
586
|
+
similarity: parseInt(similaritySlider.value),
|
|
587
|
+
style: parseInt(styleSlider.value)
|
|
588
|
+
};
|
|
589
|
+
await synthos.shared.data.save(TABLE, data);
|
|
590
|
+
const idx = profiles.findIndex(p => p.id === currentId);
|
|
591
|
+
if (idx >= 0) profiles[idx] = data;
|
|
592
|
+
isDirty = false;
|
|
593
|
+
updateSaveBtn();
|
|
594
|
+
showSaveToast();
|
|
595
|
+
renderList();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
$('deleteVoiceBtn').addEventListener('click', () => {
|
|
599
|
+
if (currentId) $('deleteConfirmDialog').classList.add('flm-dialog-overlay--open');
|
|
600
|
+
});
|
|
601
|
+
$('deleteConfirmCancel').addEventListener('click', () => {
|
|
602
|
+
$('deleteConfirmDialog').classList.remove('flm-dialog-overlay--open');
|
|
603
|
+
});
|
|
604
|
+
$('deleteConfirmOk').addEventListener('click', async () => {
|
|
605
|
+
$('deleteConfirmDialog').classList.remove('flm-dialog-overlay--open');
|
|
606
|
+
if (!currentId) return;
|
|
607
|
+
await synthos.shared.data.remove(TABLE, currentId);
|
|
608
|
+
profiles = profiles.filter(p => p.id !== currentId);
|
|
609
|
+
showInstructions();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// ============ VOICE PICKER DIALOG ============
|
|
613
|
+
function openVoicePickerDialog() {
|
|
614
|
+
$('voicePickerDialog').classList.add('flm-dialog-overlay--open');
|
|
615
|
+
}
|
|
616
|
+
function closeVoicePickerDialog() {
|
|
617
|
+
$('voicePickerDialog').classList.remove('flm-dialog-overlay--open');
|
|
618
|
+
}
|
|
619
|
+
$('changeVoiceBtn').addEventListener('click', openVoicePickerDialog);
|
|
620
|
+
$('voicePickerClose').addEventListener('click', closeVoicePickerDialog);
|
|
621
|
+
$('voicePickerCancel').addEventListener('click', closeVoicePickerDialog);
|
|
622
|
+
|
|
623
|
+
// ============ VOICE LOADING ============
|
|
624
|
+
let filterTimeout;
|
|
625
|
+
function onFilterChange() { clearTimeout(filterTimeout); filterTimeout = setTimeout(() => { resetPagination(); loadVoices(); }, 300); }
|
|
626
|
+
$('searchFilter').addEventListener('input', onFilterChange);
|
|
627
|
+
$('voiceTypeFilter').addEventListener('change', onFilterChange);
|
|
628
|
+
$('categoryFilter').addEventListener('change', onFilterChange);
|
|
629
|
+
$('sortFilter').addEventListener('change', onFilterChange);
|
|
630
|
+
$('sortDirectionFilter').addEventListener('change', onFilterChange);
|
|
631
|
+
|
|
632
|
+
$('prevPageBtn').addEventListener('click', () => { if (currentPageIndex > 0) { currentPageIndex--; currentPageToken = pageTokenHistory[currentPageIndex]; loadVoices(); } });
|
|
633
|
+
$('nextPageBtn').addEventListener('click', () => { if (hasMore && pageTokenHistory[currentPageIndex + 1]) { currentPageIndex++; currentPageToken = pageTokenHistory[currentPageIndex]; loadVoices(); } });
|
|
634
|
+
|
|
635
|
+
function resetPagination() { currentPageToken = null; pageTokenHistory = [null]; currentPageIndex = 0; hasMore = false; }
|
|
636
|
+
|
|
637
|
+
async function loadVoices() {
|
|
638
|
+
try {
|
|
639
|
+
voiceCardList.innerHTML = '<div class="voice-card-list-empty">Loading voices...</div>';
|
|
640
|
+
const params = new URLSearchParams();
|
|
641
|
+
params.append('page_size', '50');
|
|
642
|
+
params.append('include_total_count', 'true');
|
|
643
|
+
if (currentPageToken) params.append('next_page_token', currentPageToken);
|
|
644
|
+
const search = $('searchFilter').value.trim();
|
|
645
|
+
if (search) params.append('search', search);
|
|
646
|
+
const vType = $('voiceTypeFilter').value;
|
|
647
|
+
if (vType) params.append('voice_type', vType);
|
|
648
|
+
const cat = $('categoryFilter').value;
|
|
649
|
+
if (cat) params.append('category', cat);
|
|
650
|
+
const sort = $('sortFilter').value;
|
|
651
|
+
if (sort) params.append('sort', sort);
|
|
652
|
+
const dir = $('sortDirectionFilter').value;
|
|
653
|
+
if (dir) params.append('sort_direction', dir);
|
|
654
|
+
|
|
655
|
+
const data = await synthos.connectors.call('elevenlabs', 'GET', '/v2/voices?' + params.toString());
|
|
656
|
+
voicesData = data.voices || [];
|
|
657
|
+
hasMore = data.has_more || false;
|
|
658
|
+
totalVoiceCount = data.total_count || voicesData.length;
|
|
659
|
+
if (data.next_page_token && !pageTokenHistory.includes(data.next_page_token)) {
|
|
660
|
+
pageTokenHistory[currentPageIndex + 1] = data.next_page_token;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Render voice cards
|
|
664
|
+
renderVoiceCards();
|
|
665
|
+
|
|
666
|
+
// Pagination
|
|
667
|
+
const pageSize = 50;
|
|
668
|
+
const totalPages = Math.ceil(totalVoiceCount / pageSize);
|
|
669
|
+
const startNum = currentPageIndex * pageSize + 1;
|
|
670
|
+
const endNum = Math.min(startNum + voicesData.length - 1, totalVoiceCount);
|
|
671
|
+
$('voiceCount').textContent = voicesData.length > 0 ? `${startNum}-${endNum} of ${totalVoiceCount} voices` : '';
|
|
672
|
+
$('paginationControls').style.display = totalPages > 1 ? 'flex' : 'none';
|
|
673
|
+
$('prevPageBtn').disabled = currentPageIndex === 0;
|
|
674
|
+
$('nextPageBtn').disabled = !hasMore;
|
|
675
|
+
$('pageInfo').textContent = `Page ${currentPageIndex + 1} of ${totalPages}`;
|
|
676
|
+
|
|
677
|
+
// Restore voice selection for current profile
|
|
678
|
+
if (currentId) {
|
|
679
|
+
const p = profiles.find(x => x.id === currentId);
|
|
680
|
+
if (p) await resolveVoiceSelection(p);
|
|
681
|
+
}
|
|
682
|
+
} catch (e) {
|
|
683
|
+
voiceCardList.innerHTML = '<div class="voice-card-list-empty">Error loading voices</div>';
|
|
684
|
+
$('voiceCount').textContent = '';
|
|
685
|
+
$('paginationControls').style.display = 'none';
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
// ============ TEST TTS ============
|
|
691
|
+
function updateCharCount() { charCount.textContent = textInput.value.length; }
|
|
692
|
+
textInput.addEventListener('input', updateCharCount);
|
|
693
|
+
|
|
694
|
+
generateBtn.addEventListener('click', async () => {
|
|
695
|
+
// If voice ID not resolved yet, try once more on demand
|
|
696
|
+
if (!selectedVoiceId && currentId) {
|
|
697
|
+
const p = profiles.find(x => x.id === currentId);
|
|
698
|
+
if (p) await resolveVoiceSelection(p);
|
|
699
|
+
}
|
|
700
|
+
const voiceId = selectedVoiceId;
|
|
701
|
+
const text = textInput.value.trim();
|
|
702
|
+
if (!voiceId) { showStatus('Select a voice first', 'error'); return; }
|
|
703
|
+
if (!text) { showStatus('Enter text to test', 'error'); return; }
|
|
704
|
+
|
|
705
|
+
generateBtn.disabled = true;
|
|
706
|
+
generateBtn.textContent = 'Generating...';
|
|
707
|
+
showStatus('Generating speech...', 'info');
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
const res = await fetch('/api/connectors', {
|
|
711
|
+
method: 'POST',
|
|
712
|
+
headers: { 'Content-Type': 'application/json' },
|
|
713
|
+
body: JSON.stringify({
|
|
714
|
+
connector: 'elevenlabs',
|
|
715
|
+
method: 'POST',
|
|
716
|
+
path: `/v1/text-to-speech/${voiceId}`,
|
|
717
|
+
headers: { 'Accept': 'audio/mpeg' },
|
|
718
|
+
body: {
|
|
719
|
+
text,
|
|
720
|
+
model_id: modelSelect.value,
|
|
721
|
+
voice_settings: {
|
|
722
|
+
stability: stabilitySlider.value / 100,
|
|
723
|
+
similarity_boost: similaritySlider.value / 100,
|
|
724
|
+
style: styleSlider.value / 100,
|
|
725
|
+
use_speaker_boost: true
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
if (!res.ok) {
|
|
732
|
+
const errText = await res.text();
|
|
733
|
+
let errMsg = 'Generation failed';
|
|
734
|
+
try { const j = JSON.parse(errText); errMsg = j.detail?.message || j.error || errMsg; } catch (e) {}
|
|
735
|
+
throw new Error(errMsg);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
currentAudioBlob = await res.blob();
|
|
739
|
+
audioPlayer.src = URL.createObjectURL(currentAudioBlob);
|
|
740
|
+
audioSection.style.display = '';
|
|
741
|
+
createWaveform();
|
|
742
|
+
hideStatus();
|
|
743
|
+
audioPlayer.play();
|
|
744
|
+
} catch (e) {
|
|
745
|
+
showStatus('Error: ' + e.message, 'error');
|
|
746
|
+
} finally {
|
|
747
|
+
generateBtn.disabled = false;
|
|
748
|
+
generateBtn.textContent = 'Generate Speech';
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// ============ WAVEFORM ============
|
|
753
|
+
function createWaveform() {
|
|
754
|
+
waveformContainer.innerHTML = '';
|
|
755
|
+
for (let i = 0; i < 40; i++) {
|
|
756
|
+
const bar = document.createElement('div');
|
|
757
|
+
bar.className = 'waveform-bar';
|
|
758
|
+
bar.style.height = '10px';
|
|
759
|
+
waveformContainer.appendChild(bar);
|
|
760
|
+
}
|
|
761
|
+
audioPlayer.addEventListener('play', animateWaveform);
|
|
762
|
+
audioPlayer.addEventListener('pause', stopWaveform);
|
|
763
|
+
audioPlayer.addEventListener('ended', stopWaveform);
|
|
764
|
+
}
|
|
765
|
+
let waveformInterval;
|
|
766
|
+
function animateWaveform() {
|
|
767
|
+
const bars = waveformContainer.querySelectorAll('.waveform-bar');
|
|
768
|
+
waveformInterval = setInterval(() => { bars.forEach(b => { b.style.height = Math.random() * 40 + 10 + 'px'; }); }, 100);
|
|
769
|
+
}
|
|
770
|
+
function stopWaveform() {
|
|
771
|
+
clearInterval(waveformInterval);
|
|
772
|
+
waveformContainer.querySelectorAll('.waveform-bar').forEach(b => { b.style.height = '10px'; });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ============ DOWNLOAD ============
|
|
776
|
+
$('downloadBtn').addEventListener('click', () => {
|
|
777
|
+
if (!currentAudioBlob) return;
|
|
778
|
+
const url = URL.createObjectURL(currentAudioBlob);
|
|
779
|
+
const a = document.createElement('a');
|
|
780
|
+
a.href = url;
|
|
781
|
+
a.download = 'speech.mp3';
|
|
782
|
+
document.body.appendChild(a);
|
|
783
|
+
a.click();
|
|
784
|
+
document.body.removeChild(a);
|
|
785
|
+
URL.revokeObjectURL(url);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// ============ STATUS HELPERS ============
|
|
789
|
+
function showStatus(msg, type) {
|
|
790
|
+
const cls = type === 'error' ? 'flm-messagebar--error' : type === 'success' ? 'flm-messagebar--success' : 'flm-messagebar--info';
|
|
791
|
+
statusMsg.className = 'flm-messagebar ' + cls;
|
|
792
|
+
statusMsg.textContent = msg;
|
|
793
|
+
statusMsg.style.display = '';
|
|
794
|
+
}
|
|
795
|
+
function hideStatus() { statusMsg.style.display = 'none'; }
|
|
796
|
+
|
|
797
|
+
function escHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
|
|
798
|
+
</script>
|
|
799
|
+
<script id="page-helpers" src="/api/page-helpers.js?v=3" data-locked="true"></script>
|
|
800
|
+
<script id="page-script" src="/api/page-script.js?v=3" data-locked="true"></script>
|
|
801
|
+
</body></html>
|