@@ -18,20 +18,15 @@ import {
|
||||
audioPlayInfoRepository,
|
||||
audioReasonToText,
|
||||
type AudioLanguage,
|
||||
type AudioPlayInfo,
|
||||
type AudioPlayInfoRepository,
|
||||
type GuideStopInfoRequest,
|
||||
type AudioTextInfo
|
||||
type AudioPlayInfoRepository
|
||||
} from '@/repositories/AudioPlayInfoRepository'
|
||||
import type {
|
||||
GuideStopInfo
|
||||
GuideAudioVersion,
|
||||
GuideStopDetail
|
||||
} from '@/data/adapters/guideStopInfoAdapter'
|
||||
import {
|
||||
dataSourceConfig
|
||||
} from '@/config/dataSource'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import {
|
||||
resolveGuideAudioLanguages,
|
||||
resolveGuideAudioOption
|
||||
@@ -43,14 +38,15 @@ export interface ExplainAudioSelection {
|
||||
media: MediaAsset | null
|
||||
playable: boolean
|
||||
unavailableMessage?: string
|
||||
playInfo?: AudioPlayInfo
|
||||
}
|
||||
|
||||
export interface ExplainDetailEntryRequest {
|
||||
exhibitId: string
|
||||
stopId?: string
|
||||
targetType?: AudioPlayTargetType
|
||||
targetId?: string
|
||||
lang?: AudioLanguage
|
||||
version?: GuideAudioVersion
|
||||
// Navigation context is non-authoritative and keeps deep links independent of catalog loading.
|
||||
hallId?: string
|
||||
hallName?: string
|
||||
@@ -61,7 +57,13 @@ export interface ExplainDetailEntryRequest {
|
||||
|
||||
export interface ExplainTextSelection {
|
||||
exhibit: MuseumExhibit
|
||||
textInfo: AudioTextInfo
|
||||
textInfo: {
|
||||
available: boolean
|
||||
lang: AudioLanguage
|
||||
text?: string
|
||||
textLength?: number
|
||||
textHash?: string
|
||||
}
|
||||
available: boolean
|
||||
unavailableMessage?: string
|
||||
}
|
||||
@@ -96,54 +98,17 @@ export class ExplainUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveAudioTarget(exhibit: MuseumExhibit, track?: ExplainTrack | null) {
|
||||
const targetType: AudioPlayTargetType = track?.playTargetType
|
||||
|| exhibit.playTargetType
|
||||
|| 'ITEM'
|
||||
const targetId = track?.playTargetId
|
||||
|| exhibit.playTargetId
|
||||
|| exhibit.id
|
||||
|
||||
return { targetType, targetId }
|
||||
}
|
||||
|
||||
private resolveStopInfoAudioTarget(stopInfo: GuideStopInfo) {
|
||||
return {
|
||||
targetType: stopInfo.playTargetType || stopInfo.targetType,
|
||||
targetId: stopInfo.playTargetId || stopInfo.targetId
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveDetailEntryTarget(request: ExplainDetailEntryRequest): Promise<Required<GuideStopInfoRequest>> {
|
||||
const lang = request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
if (request.targetType && request.targetId) {
|
||||
return {
|
||||
targetType: request.targetType,
|
||||
targetId: request.targetId,
|
||||
lang,
|
||||
lightweight: true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetType: 'ITEM',
|
||||
targetId: request.exhibitId,
|
||||
lang,
|
||||
lightweight: true
|
||||
}
|
||||
private resolveDetailStopId(request: ExplainDetailEntryRequest) {
|
||||
return String(request.stopId || request.targetId || request.exhibitId || '').trim()
|
||||
}
|
||||
|
||||
private async resolveStaticDetailFallback(
|
||||
request: ExplainDetailEntryRequest,
|
||||
entryTarget: Required<GuideStopInfoRequest>
|
||||
entryTarget: { stopId: string }
|
||||
): Promise<MuseumExhibit | null> {
|
||||
if (entryTarget.targetType !== 'ITEM' && entryTarget.targetType !== 'STOP') {
|
||||
return null
|
||||
}
|
||||
|
||||
const candidateIds = Array.from(new Set([
|
||||
request.exhibitId,
|
||||
entryTarget.targetId
|
||||
entryTarget.stopId
|
||||
].map((id) => id?.trim()).filter(Boolean)))
|
||||
|
||||
for (const candidateId of candidateIds) {
|
||||
@@ -156,201 +121,89 @@ export class ExplainUseCase {
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
private languageLabel(language: AudioLanguage) {
|
||||
if (language === 'en-US') return '英文'
|
||||
if (language === 'yue-HK') return '粤语'
|
||||
return '中文'
|
||||
private selectTextVariant(detail: GuideStopDetail, language: AudioLanguage, version = detail.version) {
|
||||
const exact = detail.textVariants.find((variant) => (
|
||||
variant.version === version && variant.languageCode === language
|
||||
))
|
||||
if (exact) return exact
|
||||
if (language === 'yue-HK') {
|
||||
return detail.textVariants.find((variant) => variant.version === version && variant.languageCode === 'zh-CN')
|
||||
}
|
||||
return detail.textVariants.find((variant) => variant.version === version && variant.languageCode === 'zh-CN')
|
||||
}
|
||||
|
||||
private applyLanguageVariant(exhibit: MuseumExhibit, language: AudioLanguage): MuseumExhibit {
|
||||
const variant = exhibit.audioVariants?.[language]
|
||||
const mandarinText = exhibit.audioVariants?.['zh-CN']?.text
|
||||
|| exhibit.guideText
|
||||
|| exhibit.description
|
||||
const supportedLanguages = exhibit.supportedLanguages?.length
|
||||
? exhibit.supportedLanguages
|
||||
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
|
||||
|
||||
if (!variant) {
|
||||
const label = this.languageLabel(language)
|
||||
const guideText = language === 'yue-HK'
|
||||
? mandarinText || '当前讲解词暂未配置。'
|
||||
: `${label}讲解词暂未配置。`
|
||||
return {
|
||||
...exhibit,
|
||||
guideText,
|
||||
audioUrl: undefined,
|
||||
audioDuration: undefined,
|
||||
audioLanguage: language,
|
||||
audioHasText: language === 'yue-HK' && Boolean(mandarinText),
|
||||
audioAvailable: false,
|
||||
audioStatus: 'MISSING',
|
||||
audioUnavailableReason: `当前讲解暂无${label}音频`,
|
||||
supportedLanguages
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...exhibit,
|
||||
guideTitle: variant.title || exhibit.guideTitle,
|
||||
guideText: language === 'yue-HK'
|
||||
? mandarinText || '当前讲解词暂未配置。'
|
||||
: variant.text || `${this.languageLabel(language)}讲解词暂未配置。`,
|
||||
audioUrl: variant.audioUrl,
|
||||
audioDuration: variant.audioDuration,
|
||||
audioLanguage: language,
|
||||
audioHasText: language === 'yue-HK' ? Boolean(mandarinText) : variant.hasText === true,
|
||||
audioAvailable: variant.available === true && Boolean(variant.audioUrl),
|
||||
audioStatus: variant.available && variant.audioUrl ? 'READY' : 'MISSING',
|
||||
audioUnavailableReason: variant.available && variant.audioUrl
|
||||
? undefined
|
||||
: `当前讲解暂无${this.languageLabel(language)}音频`,
|
||||
supportedLanguages
|
||||
}
|
||||
}
|
||||
|
||||
private toExhibitFromStopInfo(
|
||||
stopInfo: GuideStopInfo,
|
||||
private toExhibitFromStopDetail(
|
||||
detail: GuideStopDetail,
|
||||
fallback?: MuseumExhibit | null,
|
||||
navigationContext?: ExplainDetailEntryRequest
|
||||
navigationContext?: ExplainDetailEntryRequest,
|
||||
language: AudioLanguage = 'zh-CN',
|
||||
gender: GuideAudioGender = 'female'
|
||||
): MuseumExhibit {
|
||||
const linkedPrimary = stopInfo.linkedExhibits[0]
|
||||
const coverImage = stopInfo.imageStatus === 'READY'
|
||||
? stopInfo.coverImageUrl
|
||||
: undefined
|
||||
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
|
||||
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
|
||||
// Product defaults Chinese and English to male even if the management default is female.
|
||||
const audioOption = resolveGuideAudioOption(stopInfo.audioOptions, stopInfo.lang, 'male')
|
||||
const audioAvailable = Boolean(audioOption?.playUrl) || stopInfo.audioStatus === 'READY'
|
||||
const linkedPrimary = detail.linkedExhibits[0]
|
||||
const textVariant = this.selectTextVariant(detail, language)
|
||||
const currentVersionTracks = detail.audioTracks.filter((track) => track.version === detail.version)
|
||||
const recommendedTrack = currentVersionTracks.find((track) => track.channelCode === detail.recommendedTrackCode)
|
||||
const preferredGender = language === 'yue-HK'
|
||||
? 'female'
|
||||
: recommendedTrack?.languageCode === language ? recommendedTrack.gender : gender
|
||||
const audioOption = resolveGuideAudioOption(currentVersionTracks, language, preferredGender)
|
||||
const audioAvailable = Boolean(audioOption?.playUrl)
|
||||
const supportedLanguages = resolveGuideAudioLanguages(
|
||||
stopInfo.audioOptions,
|
||||
stopInfo.supportedLanguages
|
||||
currentVersionTracks,
|
||||
currentVersionTracks.map((track) => track.languageCode)
|
||||
)
|
||||
|
||||
return {
|
||||
...(fallback || {}),
|
||||
id: fallback?.id || linkedPrimary?.id || stopInfo.targetId,
|
||||
name: stopInfo.title || fallback?.name || linkedPrimary?.name || '讲解内容',
|
||||
id: detail.id,
|
||||
name: detail.name || fallback?.name || linkedPrimary?.name || '讲解内容',
|
||||
hallId: fallback?.hallId || navigationContext?.hallId,
|
||||
hallName: fallback?.hallName || navigationContext?.hallName,
|
||||
floorId: stopInfo.floorId || fallback?.floorId || navigationContext?.floorId,
|
||||
floorId: detail.floorId || fallback?.floorId || navigationContext?.floorId,
|
||||
floorLabel: fallback?.floorLabel || navigationContext?.floorLabel,
|
||||
image: coverImage,
|
||||
description,
|
||||
poiId: stopInfo.poiId || fallback?.poiId || navigationContext?.poiId,
|
||||
sourcePoiId: stopInfo.poiId || fallback?.sourcePoiId,
|
||||
mapX: stopInfo.mapX ?? fallback?.mapX,
|
||||
mapY: stopInfo.mapY ?? fallback?.mapY,
|
||||
location: fallback?.location,
|
||||
year: fallback?.year,
|
||||
material: fallback?.material,
|
||||
size: fallback?.size,
|
||||
tags: fallback?.tags,
|
||||
guideTitle: stopInfo.title || fallback?.guideTitle,
|
||||
guideText: stopInfo.description || fallback?.guideText || fallback?.description,
|
||||
image: detail.imageStatus === 'READY' ? detail.coverImageUrl : undefined,
|
||||
description: detail.description || fallback?.description || '该讲解暂无简介。',
|
||||
poiId: detail.poiId || fallback?.poiId || navigationContext?.poiId,
|
||||
sourcePoiId: detail.poiId || fallback?.sourcePoiId,
|
||||
mapX: detail.mapX ?? fallback?.mapX,
|
||||
mapY: detail.mapY ?? fallback?.mapY,
|
||||
guideTitle: detail.name,
|
||||
guideText: textVariant?.text || detail.description || fallback?.guideText || fallback?.description,
|
||||
audioUrl: audioOption?.playUrl,
|
||||
audioDuration: audioOption?.duration,
|
||||
audioLanguage: stopInfo.lang,
|
||||
audioHasText: stopInfo.hasText,
|
||||
audioText: undefined,
|
||||
audioTextLength: undefined,
|
||||
audioTextHash: undefined,
|
||||
audioNarrationTier: undefined,
|
||||
audioUnavailableReason: audioAvailable
|
||||
? undefined
|
||||
: audioReasonToText(stopInfo.reason || (stopInfo.audioStatus === 'MISSING' ? 'NO_PUBLISHED_AUDIO' : undefined)),
|
||||
audioLanguage: language,
|
||||
audioHasText: Boolean(textVariant?.text),
|
||||
audioText: textVariant?.text,
|
||||
audioTextLength: textVariant?.textLength,
|
||||
audioTextHash: textVariant?.textHash,
|
||||
audioUnavailableReason: audioAvailable ? undefined : audioReasonToText(detail.hasAudio ? undefined : 'NO_PUBLISHED_AUDIO'),
|
||||
audioAvailable,
|
||||
audioStatus: audioAvailable ? 'READY' : stopInfo.audioStatus,
|
||||
audioStatus: audioAvailable ? 'READY' : 'MISSING',
|
||||
supportedLanguages,
|
||||
audioOptions: stopInfo.audioOptions,
|
||||
imageStatus: stopInfo.imageStatus,
|
||||
imageSource: stopInfo.imageSource,
|
||||
galleryUrls: stopInfo.imageStatus === 'READY' ? stopInfo.galleryUrls : [],
|
||||
linkedExhibitCount: stopInfo.linkedExhibitCount,
|
||||
isSharedStop: stopInfo.isSharedStop,
|
||||
linkedExhibits: stopInfo.linkedExhibits,
|
||||
stopInfoAvailable: stopInfo.available,
|
||||
stopInfoReason: stopInfo.reason,
|
||||
resolvedStopId: stopInfo.resolvedStopId,
|
||||
playTargetType: audioTarget.targetType,
|
||||
playTargetId: audioTarget.targetId
|
||||
audioOptions: detail.audioTracks,
|
||||
textVariants: detail.textVariants,
|
||||
recommendedTrackCode: detail.recommendedTrackCode,
|
||||
guideVersion: detail.version,
|
||||
textVariantCount: detail.textVariantCount,
|
||||
audioTrackCount: detail.audioTrackCount,
|
||||
imageStatus: detail.imageStatus,
|
||||
galleryUrls: detail.imageStatus === 'READY' ? detail.galleryUrls : [],
|
||||
linkedExhibitCount: detail.linkedExhibitCount,
|
||||
isSharedStop: detail.isSharedStop,
|
||||
linkedExhibits: detail.linkedExhibits,
|
||||
stopInfoAvailable: true,
|
||||
resolvedStopId: detail.id,
|
||||
playTargetType: 'STOP',
|
||||
playTargetId: detail.id
|
||||
}
|
||||
}
|
||||
|
||||
private applyPlayInfo(
|
||||
exhibit: MuseumExhibit,
|
||||
playInfo: AudioPlayInfo
|
||||
): MuseumExhibit {
|
||||
const apiPlayable = playInfo.playable === true && Boolean(playInfo.playUrl)
|
||||
const nextAudioUrl = apiPlayable ? playInfo.playUrl || undefined : undefined
|
||||
const nextAudioDuration = typeof playInfo.duration === 'number'
|
||||
? playInfo.duration
|
||||
: exhibit.audioDuration
|
||||
|
||||
return {
|
||||
...exhibit,
|
||||
guideTitle: playInfo.title || exhibit.guideTitle,
|
||||
audioUrl: nextAudioUrl,
|
||||
audioDuration: nextAudioDuration,
|
||||
audioLanguage: playInfo.lang || exhibit.audioLanguage,
|
||||
audioSubtitleUrl: playInfo.subtitleUrl || exhibit.audioSubtitleUrl,
|
||||
audioHasText: playInfo.hasText === true || exhibit.audioHasText,
|
||||
audioNarrationTier: playInfo.narrationTier || exhibit.audioNarrationTier,
|
||||
audioUnavailableReason: apiPlayable ? undefined : audioReasonToText(playInfo.reason),
|
||||
audioAvailable: apiPlayable,
|
||||
audioStatus: apiPlayable ? 'READY' : 'MISSING'
|
||||
}
|
||||
}
|
||||
|
||||
private applyTextInfo(exhibit: MuseumExhibit, textInfo: AudioTextInfo | null): MuseumExhibit {
|
||||
if (!textInfo?.available || !textInfo.text) return exhibit
|
||||
|
||||
return {
|
||||
...exhibit,
|
||||
guideTitle: textInfo.title || exhibit.guideTitle,
|
||||
guideText: textInfo.text,
|
||||
audioText: textInfo.text,
|
||||
audioTextLength: textInfo.textLength || exhibit.audioTextLength,
|
||||
audioTextHash: textInfo.textHash || exhibit.audioTextHash,
|
||||
audioLanguage: textInfo.lang || exhibit.audioLanguage,
|
||||
audioNarrationTier: textInfo.narrationTier || exhibit.audioNarrationTier,
|
||||
audioHasText: true
|
||||
}
|
||||
}
|
||||
|
||||
private async enrichExhibitAudio(
|
||||
exhibit: MuseumExhibit,
|
||||
options: {
|
||||
includeText?: boolean
|
||||
track?: ExplainTrack | null
|
||||
} = {}
|
||||
): Promise<MuseumExhibit> {
|
||||
const track = options.track ?? await this.explain.getTrackByExhibitId(exhibit.id).catch(() => null)
|
||||
const baseExhibit = this.applyTrackAudioState(exhibit, track)
|
||||
const { targetType, targetId } = this.resolveAudioTarget(baseExhibit, track)
|
||||
|
||||
private async enrichExhibitAudio(exhibit: MuseumExhibit): Promise<MuseumExhibit> {
|
||||
if (exhibit.audioOptions?.length || !exhibit.id) return exhibit
|
||||
try {
|
||||
const playInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
||||
let nextExhibit = this.applyPlayInfo(baseExhibit, playInfo)
|
||||
|
||||
if (options.includeText && playInfo.hasText) {
|
||||
try {
|
||||
const textInfo = await this.audioPlayInfo.getTextInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang: playInfo.lang
|
||||
})
|
||||
nextExhibit = this.applyTextInfo(nextExhibit, textInfo)
|
||||
} catch (error) {
|
||||
console.warn('讲解词正文加载失败,将使用静态讲解文稿:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return nextExhibit
|
||||
} catch (error) {
|
||||
console.warn('讲解播放信息加载失败,将使用静态讲解数据:', error)
|
||||
return baseExhibit
|
||||
const detail = await this.audioPlayInfo.getStopDetail(exhibit.resolvedStopId || exhibit.id, { version: 'standard' })
|
||||
return this.toExhibitFromStopDetail(detail, exhibit)
|
||||
} catch {
|
||||
return exhibit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,10 +259,7 @@ export class ExplainUseCase {
|
||||
return baseExhibit
|
||||
}
|
||||
|
||||
return this.enrichExhibitAudio(exhibit, {
|
||||
includeText: options.includeText,
|
||||
track
|
||||
})
|
||||
return this.enrichExhibitAudio(this.applyTrackAudioState(exhibit, track))
|
||||
} catch (error) {
|
||||
console.warn('讲解详情音频状态加载失败,将使用展项原始音频状态:', error)
|
||||
return exhibit
|
||||
@@ -424,63 +274,30 @@ export class ExplainUseCase {
|
||||
}
|
||||
|
||||
async enterExplainDetail(request: ExplainDetailEntryRequest) {
|
||||
const entryTarget = await this.resolveDetailEntryTarget(request)
|
||||
// Remote stop-info is the authoritative detail source. Never make its first paint wait for catalog.
|
||||
const stopId = this.resolveDetailStopId(request)
|
||||
if (!stopId) throw new Error('讲解点 ID 不能为空')
|
||||
const detail = await this.audioPlayInfo.getStopDetail(stopId, { version: request.version || 'standard' })
|
||||
const language = request.lang || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
if (dataSourceConfig.explainContentMode === 'remote') {
|
||||
const stopInfo = await this.audioPlayInfo.getStopInfo(entryTarget)
|
||||
return this.toExhibitFromStopInfo(stopInfo, null, request)
|
||||
return this.toExhibitFromStopDetail(detail, null, request, language)
|
||||
}
|
||||
|
||||
const fallbackExhibit = await this.resolveStaticDetailFallback(request, entryTarget)
|
||||
|
||||
if (fallbackExhibit) {
|
||||
return this.applyLanguageVariant(fallbackExhibit, entryTarget.lang)
|
||||
}
|
||||
|
||||
const stopInfoResult = await this.audioPlayInfo.getStopInfo(entryTarget)
|
||||
.then((stopInfo) => ({ stopInfo, error: null }))
|
||||
.catch((error) => ({ stopInfo: null, error }))
|
||||
|
||||
if (stopInfoResult.stopInfo) {
|
||||
return this.toExhibitFromStopInfo(stopInfoResult.stopInfo, fallbackExhibit, request)
|
||||
}
|
||||
|
||||
throw stopInfoResult.error || new Error('讲解详情加载失败')
|
||||
const fallbackExhibit = await this.resolveStaticDetailFallback(request, { stopId })
|
||||
return this.toExhibitFromStopDetail(detail, fallbackExhibit, request, language)
|
||||
}
|
||||
|
||||
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
const lang = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
|
||||
try {
|
||||
const textInfo = await this.audioPlayInfo.getTextInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang
|
||||
})
|
||||
const nextExhibit = this.applyTextInfo(exhibit, textInfo)
|
||||
|
||||
return {
|
||||
exhibit: nextExhibit,
|
||||
textInfo,
|
||||
available: textInfo.available === true && Boolean(textInfo.text),
|
||||
unavailableMessage: textInfo.available ? undefined : audioReasonToText(textInfo.reason || 'NO_TEXT')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('讲解词正文解析失败:', error)
|
||||
return {
|
||||
exhibit,
|
||||
textInfo: {
|
||||
available: false,
|
||||
targetType,
|
||||
targetId,
|
||||
lang,
|
||||
reason: 'SERVICE_ERROR'
|
||||
},
|
||||
available: false,
|
||||
unavailableMessage: '讲解词服务暂不可用,请稍后重试'
|
||||
}
|
||||
const variants = exhibit.textVariants || []
|
||||
const version = exhibit.guideVersion || 'standard'
|
||||
const variant = variants.find((item) => item.version === version && item.languageCode === lang)
|
||||
|| (lang === 'yue-HK' ? variants.find((item) => item.version === version && item.languageCode === 'zh-CN') : undefined)
|
||||
const text = variant?.text || (lang === 'yue-HK' ? exhibit.textVariants?.find((item) => item.languageCode === 'zh-CN')?.text : undefined)
|
||||
const nextExhibit = text ? { ...exhibit, guideText: text, audioText: text, audioHasText: true } : exhibit
|
||||
return {
|
||||
exhibit: nextExhibit,
|
||||
textInfo: { available: Boolean(text), lang, text, textLength: variant?.textLength, textHash: variant?.textHash },
|
||||
available: Boolean(text),
|
||||
unavailableMessage: text ? undefined : audioReasonToText('NO_TEXT')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,11 +371,9 @@ export class ExplainUseCase {
|
||||
}
|
||||
|
||||
openGuideStopDetail(stopId: string) {
|
||||
const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId })
|
||||
return this.enterExplainDetail({
|
||||
exhibitId: target.targetId,
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId
|
||||
exhibitId: stopId,
|
||||
stopId
|
||||
})
|
||||
}
|
||||
|
||||
@@ -578,86 +393,40 @@ export class ExplainUseCase {
|
||||
const exhibit = detailExhibit || summaryExhibit
|
||||
if (!exhibit) return null
|
||||
|
||||
const targetType: AudioPlayTargetType = track?.playTargetType
|
||||
|| exhibit.playTargetType
|
||||
|| 'ITEM'
|
||||
const targetId = track?.playTargetId
|
||||
|| exhibit.playTargetId
|
||||
|| exhibit.id
|
||||
|
||||
const toPlayableSelection = (
|
||||
playInfo: AudioPlayInfo,
|
||||
requestTargetType: AudioPlayTargetType,
|
||||
requestTargetId: string
|
||||
): ExplainAudioSelection => {
|
||||
const media: MediaAsset = {
|
||||
id: `play-${playInfo.audioId || `${requestTargetType}-${requestTargetId}`}`,
|
||||
type: 'audio',
|
||||
url: playInfo.playUrl || undefined,
|
||||
duration: typeof playInfo.duration === 'number' ? playInfo.duration : undefined,
|
||||
language: playInfo.lang,
|
||||
available: true
|
||||
}
|
||||
|
||||
return {
|
||||
exhibit,
|
||||
track,
|
||||
media,
|
||||
playable: true,
|
||||
playInfo
|
||||
}
|
||||
const language = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
const option = resolveGuideAudioOption(exhibit.audioOptions, language, 'female')
|
||||
if (!option?.playUrl) {
|
||||
return { exhibit, track, media: null, playable: false, unavailableMessage: audioReasonToText('NO_PUBLISHED_AUDIO') }
|
||||
}
|
||||
|
||||
try {
|
||||
const initialPlayInfo = await this.audioPlayInfo.getPlayInfo({ targetType, targetId })
|
||||
|
||||
if (initialPlayInfo.playable && initialPlayInfo.playUrl) {
|
||||
return toPlayableSelection(initialPlayInfo, targetType, targetId)
|
||||
}
|
||||
|
||||
return {
|
||||
exhibit,
|
||||
track,
|
||||
media: null,
|
||||
playable: false,
|
||||
playInfo: initialPlayInfo,
|
||||
unavailableMessage: audioReasonToText(initialPlayInfo.reason)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('语音播放解析失败:', error)
|
||||
const serviceErrorPlayInfo: AudioPlayInfo = {
|
||||
playable: false,
|
||||
targetType,
|
||||
targetId,
|
||||
lang: dataSourceConfig.audioLanguage as AudioPlayInfo['lang'],
|
||||
hasText: false,
|
||||
fallback: false,
|
||||
reason: 'SERVICE_ERROR'
|
||||
}
|
||||
return {
|
||||
exhibit,
|
||||
track,
|
||||
media: null,
|
||||
playable: false,
|
||||
playInfo: serviceErrorPlayInfo,
|
||||
unavailableMessage: '语音服务暂不可用,当前提供图文讲解'
|
||||
}
|
||||
return {
|
||||
exhibit,
|
||||
track,
|
||||
media: {
|
||||
id: `channel-${option.channelCode}`,
|
||||
type: 'audio',
|
||||
url: option.playUrl,
|
||||
duration: option.duration,
|
||||
language,
|
||||
available: true
|
||||
},
|
||||
playable: true
|
||||
}
|
||||
}
|
||||
|
||||
async selectAudioForExplainDetail(
|
||||
exhibit: MuseumExhibit,
|
||||
options: {
|
||||
refreshPlayInfo?: boolean
|
||||
voiceGender?: GuideAudioGender
|
||||
} = {}
|
||||
): Promise<ExplainAudioSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
const language = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
const audioOption = options.refreshPlayInfo
|
||||
? undefined
|
||||
: resolveGuideAudioOption(exhibit.audioOptions, language, options.voiceGender || 'male')
|
||||
const version = exhibit.guideVersion || 'standard'
|
||||
const optionsForVersion = (exhibit.audioOptions || []).filter((option) => !option.version || option.version === version)
|
||||
const recommendedTrack = optionsForVersion.find((option) => option.channelCode === exhibit.recommendedTrackCode)
|
||||
const preferredGender = language === 'yue-HK'
|
||||
? 'female'
|
||||
: options.voiceGender || (recommendedTrack?.languageCode === language ? recommendedTrack.gender : 'female')
|
||||
const audioOption = resolveGuideAudioOption(optionsForVersion, language, preferredGender)
|
||||
|
||||
if (audioOption) {
|
||||
const audioExhibit: MuseumExhibit = {
|
||||
@@ -683,12 +452,12 @@ export class ExplainUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.refreshPlayInfo && exhibit.audioUrl?.trim()) {
|
||||
if (exhibit.audioUrl?.trim()) {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: {
|
||||
id: `static-${targetType}-${targetId}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
|
||||
id: `static-${exhibit.resolvedStopId || exhibit.id}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
|
||||
type: 'audio',
|
||||
url: exhibit.audioUrl,
|
||||
duration: exhibit.audioDuration,
|
||||
@@ -699,58 +468,12 @@ export class ExplainUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
if (exhibit.audioStatus && exhibit.audioStatus !== 'READY') {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
unavailableMessage: audioReasonToText(exhibit.stopInfoReason || 'NO_PUBLISHED_AUDIO')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const playInfo = await this.audioPlayInfo.getPlayInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang: language,
|
||||
refresh: options.refreshPlayInfo === true
|
||||
})
|
||||
|
||||
if (!playInfo.playable || !playInfo.playUrl) {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
playInfo,
|
||||
unavailableMessage: audioReasonToText(playInfo.reason)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: {
|
||||
id: `play-${playInfo.audioId || `${targetType}-${targetId}`}`,
|
||||
type: 'audio',
|
||||
url: playInfo.playUrl,
|
||||
duration: typeof playInfo.duration === 'number' ? playInfo.duration : undefined,
|
||||
language: playInfo.lang,
|
||||
available: true
|
||||
},
|
||||
playable: true,
|
||||
playInfo
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('语音播放解析失败:', error)
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
unavailableMessage: '语音服务暂不可用,请稍后重试'
|
||||
}
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: null,
|
||||
playable: false,
|
||||
unavailableMessage: audioReasonToText('NO_PUBLISHED_AUDIO')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user