diff --git a/apps/stage-pocket/src/App.vue b/apps/stage-pocket/src/App.vue
index 5079c0db7..ec4916488 100644
--- a/apps/stage-pocket/src/App.vue
+++ b/apps/stage-pocket/src/App.vue
@@ -10,7 +10,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
-import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
@@ -57,8 +56,7 @@ async function removeAuthenticationProviderConfiguration() {
if (!syncedPinia.isLeader())
return
- if (await unconfigureAuthenticationProviders())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(false)
}
function registerAuthenticatedSetup() {
@@ -66,8 +64,7 @@ function registerAuthenticatedSetup() {
if (!syncedPinia.isLeader())
return
- if (await configureAsDefaultsIfEmpty())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(true)
await onboardingStore.closeAfterAuthentication()
})
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue
index 2c470499c..67ca8170f 100644
--- a/apps/stage-tamagotchi/src/renderer/App.vue
+++ b/apps/stage-tamagotchi/src/renderer/App.vue
@@ -19,7 +19,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
-import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
@@ -142,8 +141,7 @@ function createFullStageRuntime() {
if (!syncedPinia.isLeader())
return
- if (await unconfigureAuthenticationProviders())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(false)
}
function registerAuthenticatedSetup() {
@@ -151,8 +149,7 @@ function createFullStageRuntime() {
if (!syncedPinia.isLeader())
return
- if (await configureAsDefaultsIfEmpty())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(true)
await onboardingStore.closeAfterAuthentication()
})
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
diff --git a/apps/stage-web/src/App.vue b/apps/stage-web/src/App.vue
index 6a05eefe8..768bc5308 100644
--- a/apps/stage-web/src/App.vue
+++ b/apps/stage-web/src/App.vue
@@ -12,7 +12,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
-import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
@@ -62,8 +61,7 @@ async function removeAuthenticationProviderConfiguration() {
if (!syncedPinia.isLeader())
return
- if (await unconfigureAuthenticationProviders())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(false)
}
function registerAuthenticatedSetup() {
@@ -71,8 +69,7 @@ function registerAuthenticatedSetup() {
if (!syncedPinia.isLeader())
return
- if (await configureAsDefaultsIfEmpty())
- await cardStore.persistActiveCardModuleSelections()
+ await cardStore.configureForAuthentication(true)
await onboardingStore.closeAfterAuthentication()
})
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml
index 4112e3f69..3cbf70663 100644
--- a/packages/i18n/src/locales/en/settings.yaml
+++ b/packages/i18n/src/locales/en/settings.yaml
@@ -414,10 +414,10 @@ pages:
systemprompt: You will receive messages, answer to them like a real human.
posthistoryinstructions: Remember to imitate an human.
modules_info: >-
- Configure modules for this character card. If not set, default module
- configuration will be used.
+ Configure modules for this character card. Empty fields inherit global settings.
use_default: Use default
use_default_not_configured: Use default (not configured)
+ inherit_global_settings: Inherit global settings
fields_info:
subtitle: >-
You can put here some details about the character you are creating,
@@ -441,6 +441,7 @@ pages:
consciousness_model: Select the consciousness model (AI model) for this character.
speech_model: Select the speech synthesis model and voice for this character.
errors:
+ model_required: Select a model for each explicit provider. Models cannot be inherited from a different provider.
name: Name should be valid or non-empty.
version: 'Error: Invalid version number !'
description: 'Error: You must provide a description for this card.'
diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml
index 3966436cd..45730f0d7 100644
--- a/packages/i18n/src/locales/zh-Hans/settings.yaml
+++ b/packages/i18n/src/locales/zh-Hans/settings.yaml
@@ -399,9 +399,10 @@ pages:
systemprompt: 你将收到消息,请像真实人类一样回复。
posthistoryinstructions: 记得模仿人类的行为。
modules_info: >-
- 为此角色卡配置模块。如未设置,将使用默认模块配置。
+ 配置此角色卡的模块。空字段将继承全局设置。
use_default: 使用默认
use_default_not_configured: 使用默认(未配置)
+ inherit_global_settings: 继承全局设置
fields_info:
subtitle: >-
您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。
@@ -421,6 +422,7 @@ pages:
consciousness_model: 为这个角色选择 AI 模型。
speech_model: 为此角色指定语音合成模型与音色。
errors:
+ model_required: 请为指定的提供商选择模型,不能继承其他提供商的模型。
name: 错误:你必须提供一个有效的名称!
version: '错误:版本号无效!'
description: '错误:你必须为此卡片提供描述。'
diff --git a/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue b/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
index 048fdabdf..308d6314f 100644
--- a/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
+++ b/packages/stage-pages/src/pages/settings/airi-card/components/CardCreationDialog.vue
@@ -1,19 +1,19 @@
@@ -574,9 +628,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.chat.provider') }}
@@ -588,10 +642,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.consciousness.model') }}
@@ -603,9 +656,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.vision.provider') }}
@@ -617,10 +670,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.vision.model') }}
@@ -632,9 +684,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.speech.provider') }}
@@ -646,10 +698,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.speech.model') }}
@@ -661,10 +712,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.speech.voice') }}
@@ -676,9 +726,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
{{ t('settings.pages.card.body-model') }}
@@ -695,7 +745,7 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
diff --git a/packages/stage-pages/src/pages/settings/airi-card/components/CardDetailDialog.vue b/packages/stage-pages/src/pages/settings/airi-card/components/CardDetailDialog.vue
index d1141cc22..7661efce7 100644
--- a/packages/stage-pages/src/pages/settings/airi-card/components/CardDetailDialog.vue
+++ b/packages/stage-pages/src/pages/settings/airi-card/components/CardDetailDialog.vue
@@ -9,9 +9,6 @@ import { exportAiriCardPackage } from '@proj-airi/stage-ui/services/airi-card-im
import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
-import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
-import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
-import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
import { Button, Select } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import {
@@ -41,17 +38,11 @@ const emit = defineEmits<{
const { t } = useI18n()
const { trackSceneBackgroundSet } = useAnalytics()
const cardStore = useAiriCardStore()
-const consciousnessStore = useConsciousnessStore()
-const speechStore = useSpeechStore()
-const visionStore = useVisionStore()
const backgroundStore = useBackgroundStore()
const displayModelsStore = useDisplayModelsStore()
const { removeCard } = cardStore
const { activeCardId } = storeToRefs(cardStore)
-const { activeProvider: consciousnessProvider, activeModel: defaultConsciousnessModel } = storeToRefs(consciousnessStore)
-const { activeSpeechProvider: speechProvider, activeSpeechModel: defaultSpeechModel, activeSpeechVoiceId: defaultVoiceId } = storeToRefs(speechStore)
-const { activeProvider: visionProvider, activeModel: defaultVisionModel } = storeToRefs(visionStore)
const isRefreshingGallery = ref(false)
const isExportingCard = shallowRef(false)
@@ -317,15 +308,13 @@ watch(() => props.modelValue, (isOpen) => {
})
// Helper function to generate placeholder text for default values
-function getDefaultPlaceholder(defaultValue: string | undefined): string {
- return defaultValue
- ? `${t('settings.pages.card.creation.use_default')} (${defaultValue})`
- : t('settings.pages.card.creation.use_default_not_configured')
+function getDefaultPlaceholder(): string {
+ return t('settings.pages.card.creation.inherit_global_settings')
}
// Helper function to get display value for module settings
-function getModuleDisplayValue(value: string | undefined, defaultValue: string | undefined): string {
- return value || getDefaultPlaceholder(defaultValue)
+function getModuleDisplayValue(value: string | undefined): string {
+ return value || getDefaultPlaceholder()
}
@@ -466,7 +455,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.chat.provider') }}
- {{ getModuleDisplayValue(moduleSettings.consciousnessProvider, consciousnessProvider) }}
+ {{ getModuleDisplayValue(moduleSettings.consciousnessProvider) }}
@@ -483,7 +472,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.consciousness.model') }}
- {{ getModuleDisplayValue(moduleSettings.consciousness, defaultConsciousnessModel) }}
+ {{ getModuleDisplayValue(moduleSettings.consciousness) }}
@@ -500,7 +489,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.vision.provider') }}
- {{ getModuleDisplayValue(moduleSettings.visionProvider, visionProvider) }}
+ {{ getModuleDisplayValue(moduleSettings.visionProvider) }}
@@ -517,7 +506,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.vision.model') }}
- {{ getModuleDisplayValue(moduleSettings.vision, defaultVisionModel) }}
+ {{ getModuleDisplayValue(moduleSettings.vision) }}
@@ -534,7 +523,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.speech.provider') }}
- {{ getModuleDisplayValue(moduleSettings.speechProvider, speechProvider) }}
+ {{ getModuleDisplayValue(moduleSettings.speechProvider) }}
@@ -551,7 +540,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.speech.model') }}
- {{ getModuleDisplayValue(moduleSettings.speech, defaultSpeechModel) }}
+ {{ getModuleDisplayValue(moduleSettings.speech) }}
@@ -568,7 +557,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
{{ t('settings.pages.card.speech.voice') }}
- {{ getModuleDisplayValue(moduleSettings.voice, defaultVoiceId) }}
+ {{ getModuleDisplayValue(moduleSettings.voice) }}
diff --git a/packages/stage-pages/src/pages/settings/modules/consciousness.vue b/packages/stage-pages/src/pages/settings/modules/consciousness.vue
index 96562e1a8..b168b62a0 100644
--- a/packages/stage-pages/src/pages/settings/modules/consciousness.vue
+++ b/packages/stage-pages/src/pages/settings/modules/consciousness.vue
@@ -42,28 +42,26 @@ watch(activeProvider, async (provider) => {
await consciousnessStore.loadModelsForProvider(provider)
}, { immediate: true })
-watch([activeProvider, activeModel], ([provider, model]) => {
- void airiCardStore.updateActiveCardConsciousness({ provider, model })
-})
+async function persistSelection() {
+ await airiCardStore.updateActiveCardConsciousness({ provider: activeProvider.value, model: activeModel.value })
+}
function updateCustomModelName(value: string) {
customModelName.value = value
}
-function selectModel(modelId: string) {
+async function selectModel(modelId: string) {
const previousModelId = activeModel.value
activeModel.value = modelId
+ await persistSelection()
if (previousModelId !== modelId)
trackModelSwitched(previousModelId || 'none', modelId)
}
-function handleDeleteProvider(providerId: string) {
- if (activeProvider.value === providerId) {
- activeProvider.value = ''
- activeModel.value = ''
- }
- providersStore.deleteProvider(providerId)
+async function handleDeleteProvider(providerId: string) {
+ await airiCardStore.clearProviderSelections(providerId)
+ await providersStore.deleteProvider(providerId)
}
async function updateReasoning(value: boolean) {
@@ -104,6 +102,7 @@ async function updateReasoning(value: boolean) {
:value="metadata.id"
:title="metadata.localizedName || 'Unknown'"
:description="metadata.localizedDescription"
+ @update:model-value="persistSelection"
@click="trackProviderClick(metadata.id, 'consciousness')"
>
@@ -194,6 +193,7 @@ async function updateReasoning(value: boolean) {
v-model="activeModel" type="text"
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
+ @input="persistSelection"
>
@@ -285,6 +285,7 @@ async function updateReasoning(value: boolean) {
v-model="activeModel" type="text"
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
+ @input="persistSelection"
>
diff --git a/packages/stage-pages/src/pages/settings/modules/speech.vue b/packages/stage-pages/src/pages/settings/modules/speech.vue
index f62d6beb7..1f78ef11f 100644
--- a/packages/stage-pages/src/pages/settings/modules/speech.vue
+++ b/packages/stage-pages/src/pages/settings/modules/speech.vue
@@ -279,6 +279,7 @@ function selectSpeechProvider(providerId: string) {
* Tracks explicit voice selection from catalog or custom input controls.
*/
async function selectSpeechVoice(voiceId: string | undefined) {
+ await persistSelection()
if (!voiceId)
return
@@ -290,11 +291,15 @@ async function selectSpeechVoice(voiceId: string | undefined) {
})
}
-function selectSpeechSource(sourceId: string) {
+async function selectSpeechSource(sourceId: string) {
activeSpeechProvider.value = sourceId
+ activeSpeechModel.value = ''
+ activeSpeechVoiceId.value = ''
+ activeSpeechVoice.value = undefined
+ await persistSelection()
}
-function selectSpeechModel(modelOptionId: string) {
+async function selectSpeechModel(modelOptionId: string) {
const streamingModelId = modelIdFromStreamingOptionId(modelOptionId)
const nextProvider = streamingModelId == null
? activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
@@ -310,6 +315,9 @@ function selectSpeechModel(modelOptionId: string) {
}
activeSpeechModel.value = nextModel
+ activeSpeechVoiceId.value = ''
+ activeSpeechVoice.value = undefined
+ await persistSelection()
}
/**
@@ -366,26 +374,11 @@ onMounted(async () => {
trackOfficialTtsExposure()
})
-watch(activeSpeechProvider, async (newProvider, oldProvider) => {
+watch(activeSpeechProvider, async (newProvider) => {
await providersStore.loadModelsForConfiguredProviders()
+ if (newProvider !== activeSpeechProvider.value)
+ return
- // Reset model and voice when switching providers (but not on initial load)
- const isMergedOfficialSwitch = (
- oldProvider === OFFICIAL_SPEECH_PROVIDER_ID
- || oldProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
- ) && (
- newProvider === OFFICIAL_SPEECH_PROVIDER_ID
- || newProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
- )
- if (oldProvider !== undefined && oldProvider !== newProvider && !isMergedOfficialSwitch) {
- activeSpeechModel.value = ''
- activeSpeechVoiceId.value = ''
- activeSpeechVoice.value = undefined
- }
-
- // Re-seed the streaming default model after the reset above so its voices
- // load model-scoped (the server only returns recommended voices for an
- // explicit ?model=). No-op for other providers / when a model is selected.
speechStore.ensureActiveSpeechModel()
await speechStore.loadVoicesForProvider(newProvider, activeSpeechModel.value || undefined)
trackOfficialTtsExposure(newProvider, currentTtsModelId())
@@ -397,16 +390,17 @@ watch(activeSpeechModel, async (model) => {
if (!activeSpeechProvider.value)
return
- activeSpeechVoiceId.value = ''
- activeSpeechVoice.value = undefined
-
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, model || undefined)
trackOfficialTtsExposure(activeSpeechProvider.value, currentTtsModelId())
})
-watch([activeSpeechProvider, activeSpeechModel, activeSpeechVoiceId], ([provider, model, voiceId]) => {
- void airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
-})
+async function persistSelection() {
+ await airiCardStore.updateActiveCardSpeech({
+ provider: activeSpeechProvider.value,
+ model: activeSpeechModel.value,
+ voice_id: activeSpeechVoiceId.value,
+ })
+}
// Function to generate speech
async function generateTestSpeech() {
@@ -589,21 +583,17 @@ function commitCustomVoiceSelection() {
function updateCustomModelName(value: string | undefined) {
activeSpeechModel.value = value || ''
+ activeSpeechVoiceId.value = ''
+ void persistSelection()
}
-function handleDeleteProvider(providerId: string) {
+async function handleDeleteProvider(providerId: string) {
if (providerId === 'speech-noop') {
return
}
- if (activeSpeechProvider.value === providerId) {
- activeSpeechProvider.value = 'speech-noop'
- activeSpeechModel.value = ''
- activeSpeechVoiceId.value = ''
- activeSpeechVoice.value = undefined
- }
-
- providersStore.deleteProvider(providerId)
+ await airiCardStore.clearProviderSelections(providerId)
+ await providersStore.deleteProvider(providerId)
}
@@ -899,6 +889,7 @@ function handleDeleteProvider(providerId: string) {