diff --git a/frontend/src/views/VoiceStudio.vue b/frontend/src/views/VoiceStudio.vue
index 7b757e5..645da92 100644
--- a/frontend/src/views/VoiceStudio.vue
+++ b/frontend/src/views/VoiceStudio.vue
@@ -47,6 +47,7 @@ const profiles = ref([])
const universes = ref([])
const selectedProfileId = ref('')
const selectedUniverseId = ref('')
+const sessionFilter = ref<'active' | 'recent'>('active')
const textTurnInput = ref('')
const uploadTranscriptHint = ref('')
const loadingSessions = ref(false)
@@ -60,12 +61,14 @@ const recordingDurationMs = ref(0)
const error = ref('')
const mediaError = ref('')
const recorderSupported = computed(() => typeof window !== 'undefined' && 'MediaRecorder' in window)
+const sessionPollIntervalMs = 1500
let mediaRecorder: MediaRecorder | null = null
let mediaStream: MediaStream | null = null
let recordingChunks: Blob[] = []
let recordingTimer: number | null = null
let recordingStartedAt = 0
+let sessionPollTimer: number | null = null
const recordedBlob = ref(null)
const recordedAudioUrl = ref(null)
@@ -78,6 +81,22 @@ const universeOptions = computed(() =>
)
const activeTurnList = computed(() => activeSession.value?.recent_turns ?? [])
+const transcriptionModeDescription = computed(() => {
+ switch (activeSession.value?.transcription_mode_hint) {
+ case 'openai':
+ return '当前会尝试使用 OpenAI 语音转写,转写提示可以留空。'
+ case 'disabled':
+ return '当前环境禁用了真实语音转写,请先使用文本共创或填写开发转写提示。'
+ default:
+ return '当前默认是 demo 转写模式。若本地未接真实 ASR,可在下方填写转写提示辅助开发验证。'
+ }
+})
+const isSessionProcessing = computed(
+ () =>
+ activeSession.value?.status === 'processing_turn'
+ || activeSession.value?.last_turn_status === 'received'
+ || activeSession.value?.last_turn_status === 'transcribing',
+)
function formatSessionStatus(status: string) {
switch (status) {
@@ -182,8 +201,13 @@ async function loadSessions() {
if (!userStore.user) return
loadingSessions.value = true
try {
- sessions.value = await api.get('/api/voice-sessions?limit=8')
- if (!activeSession.value) {
+ const params = new URLSearchParams({
+ limit: '8',
+ active_first: 'true',
+ active_only: sessionFilter.value === 'active' ? 'true' : 'false',
+ })
+ sessions.value = await api.get(`/api/voice-sessions?${params.toString()}`)
+ if (!activeSession.value && sessionFilter.value === 'recent') {
const resumable = sessions.value.find((item) => item.can_continue)
if (resumable) {
await loadSessionDetail(resumable.id)
@@ -196,6 +220,18 @@ async function loadSessions() {
}
}
+async function loadLatestActiveSession() {
+ if (!userStore.user) return
+ try {
+ const session = await api.get('/api/voice-sessions/active')
+ if (session) {
+ await loadSessionDetail(session.id)
+ }
+ } catch {
+ // Ignore active-session bootstrap failures and fall back to normal listing.
+ }
+}
+
async function loadSessionDetail(sessionId: string) {
loadingSessionDetail.value = true
error.value = ''
@@ -208,6 +244,23 @@ async function loadSessionDetail(sessionId: string) {
}
}
+function stopSessionPolling() {
+ if (sessionPollTimer) {
+ window.clearInterval(sessionPollTimer)
+ sessionPollTimer = null
+ }
+}
+
+function startSessionPolling() {
+ if (!activeSession.value?.id || sessionPollTimer) return
+ sessionPollTimer = window.setInterval(() => {
+ if (activeSession.value?.id) {
+ void loadSessionDetail(activeSession.value.id)
+ void loadSessions()
+ }
+ }, sessionPollIntervalMs)
+}
+
async function createSession() {
creatingSession.value = true
error.value = ''
@@ -217,6 +270,7 @@ async function createSession() {
universe_id: selectedUniverseId.value || null,
target_mode: 'story',
})
+ sessionFilter.value = 'active'
await loadSessions()
await loadSessionDetail(session.id)
textTurnInput.value = ''
@@ -319,6 +373,39 @@ async function finalizeSession() {
}
}
+async function retryFailedTurn(turnId: string) {
+ if (!activeSession.value) return
+ sendingTurn.value = true
+ error.value = ''
+ try {
+ const result = await api.post(
+ `/api/voice-sessions/${activeSession.value.id}/turns/${turnId}/retry`,
+ )
+ await refreshAfterTurn(result.session_id, result.turn_id)
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '重试本轮故事失败'
+ } finally {
+ sendingTurn.value = false
+ }
+}
+
+async function retryAssistantAudio(turnId: string) {
+ if (!activeSession.value) return
+ sendingTurn.value = true
+ error.value = ''
+ try {
+ await api.post(
+ `/api/voice-sessions/${activeSession.value.id}/turns/${turnId}/retry-audio`,
+ )
+ await loadSessionDetail(activeSession.value.id)
+ await loadSessions()
+ } catch (err) {
+ error.value = err instanceof Error ? err.message : '补发语音失败'
+ } finally {
+ sendingTurn.value = false
+ }
+}
+
async function abandonSession() {
if (!activeSession.value) return
abandoning.value = true
@@ -404,16 +491,34 @@ watch(selectedProfileId, (newId) => {
}
})
+watch(sessionFilter, () => {
+ void loadSessions()
+})
+
+watch(
+ () => isSessionProcessing.value,
+ (processing) => {
+ if (processing) {
+ startSessionPolling()
+ } else {
+ stopSessionPolling()
+ }
+ },
+ { immediate: true },
+)
+
onMounted(async () => {
if (!userStore.user) {
await userStore.fetchSession()
}
if (!userStore.user) return
await fetchProfiles()
+ await loadLatestActiveSession()
await loadSessions()
})
onBeforeUnmount(() => {
+ stopSessionPolling()
if (recording.value) {
stopRecording()
}
@@ -488,6 +593,29 @@ onBeforeUnmount(() => {
{{ sessions.length }} 个
+
+
+
+
+
@@ -558,6 +686,13 @@ onBeforeUnmount(() => {
+
- 当前默认是 demo 转写模式。若本地未接真实 ASR,可在下方填写转写提示辅助开发验证。
+ {{ transcriptionModeDescription }}