diff --git a/src/data/adapters/backendExplainDataAdapter.ts b/src/data/adapters/backendExplainDataAdapter.ts index f2eda6b..15f2e33 100644 --- a/src/data/adapters/backendExplainDataAdapter.ts +++ b/src/data/adapters/backendExplainDataAdapter.ts @@ -78,6 +78,7 @@ export interface BackendHall { stopCount?: number | null linkedExhibitCount?: number | null audioReadyStopCount?: number | null + audioOptionCount?: number | null hasAudio?: boolean | null audioStatus?: string | null supportedLanguages?: string[] | null @@ -158,6 +159,7 @@ export interface BackendCatalogStopItem { hasAudio?: boolean | null audioStatus?: string | null supportedLanguages?: string[] | null + audioOptionCount?: number | null hasTextRecord?: boolean | null playTargetType?: string | null playTargetId?: string | number | null @@ -287,6 +289,7 @@ export const toCatalogHall = ( stopCount: normalizeNumber(source.stopCount), linkedExhibitCount: normalizeNumber(source.linkedExhibitCount), audioReadyStopCount: normalizeNumber(source.audioReadyStopCount), + audioOptionCount: normalizeNumber(source.audioOptionCount), hasAudio: source.hasAudio === true, audioStatus: normalizeCatalogAudioStatus(source.audioStatus), supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages), @@ -359,6 +362,7 @@ export const toCatalogGuideStop = ( hasAudio: source.hasAudio === true, audioStatus: normalizeCatalogAudioStatus(source.audioStatus), supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages), + audioOptionCount: normalizeNumber(source.audioOptionCount), hasTextRecord: source.hasTextRecord === true, poiId: stringifyId(source.poiId) || undefined, mapX: normalizeNumber(source.mapX), diff --git a/src/data/adapters/guideStopInfoAdapter.ts b/src/data/adapters/guideStopInfoAdapter.ts index 1e3f3ba..d2da604 100644 --- a/src/data/adapters/guideStopInfoAdapter.ts +++ b/src/data/adapters/guideStopInfoAdapter.ts @@ -6,6 +6,7 @@ import { } from '@/utils/publicUrl' export type GuideAudioLanguage = 'zh-CN' | 'yue-HK' | 'en-US' +export type GuideAudioVoiceGender = 'female' | 'male' export interface BackendGuideStopLinkedExhibit { id?: string | number | null @@ -42,12 +43,26 @@ export interface BackendGuideStopInfo { hasText?: boolean supportedLanguages?: string[] | null audioStatus?: string | null + audioOptions?: BackendGuideStopAudioOption[] | null languageVariants?: BackendGuideStopLanguageVariant[] | null reason?: string | null linkedExhibitCount?: number | string | null isSharedStop?: boolean | null } +export interface BackendGuideStopAudioOption { + channelCode?: string | null + displayName?: string | null + languageCode?: string | null + languageName?: string | null + gender?: string | null + playUrl?: string | null + duration?: number | string | null + format?: string | null + isDefault?: boolean | null + sortOrder?: number | string | null +} + export interface BackendGuideStopLanguageVariant { lang?: string | null enabled?: boolean | null @@ -129,6 +144,19 @@ export interface GuideStopLanguageVariant { reason?: string } +export interface GuideStopAudioOption { + channelCode: string + displayName: string + languageCode: GuideAudioLanguage + languageName?: string + gender: GuideAudioVoiceGender + playUrl: string + duration?: number + format?: string + isDefault: boolean + sortOrder?: number +} + export interface GuideStopInfo { available: boolean targetType: AudioPlayTargetType @@ -151,6 +179,7 @@ export interface GuideStopInfo { hasAudio: boolean hasText: boolean supportedLanguages: GuideAudioLanguage[] + audioOptions: GuideStopAudioOption[] languageVariants: Record audioStatus: 'READY' | 'MISSING' | string reason?: string @@ -264,12 +293,57 @@ const normalizeLanguageVariants = ( return normalizedVariants } +const normalizeVoiceGender = (value: string | null | undefined): GuideAudioVoiceGender | undefined => { + const normalized = value?.trim().toLowerCase() + return normalized === 'female' || normalized === 'male' ? normalized : undefined +} + +const normalizeAudioOptions = ( + options: BackendGuideStopAudioOption[] | null | undefined +): GuideStopAudioOption[] => ( + (options || []) + .map((option) => { + if (!isSupportedGuideAudioLanguage(option.languageCode)) return null + + const channelCode = option.channelCode?.trim() + const gender = normalizeVoiceGender(option.gender) + const playUrl = normalizeSameOriginPublicUrl(option.playUrl) || undefined + if (!channelCode || !gender || !playUrl) return null + + const languageCode = normalizeGuideAudioLanguage(option.languageCode) + const displayName = option.displayName?.trim() + || `${languageCode === 'en-US' ? '英文' : languageCode === 'yue-HK' ? '粤语' : '普通话'}${gender === 'female' ? '女声' : '男声'}` + + return { + channelCode, + displayName, + languageCode, + languageName: option.languageName?.trim() || undefined, + gender, + playUrl, + duration: normalizeNumber(option.duration), + format: option.format?.trim() || undefined, + isDefault: option.isDefault === true, + sortOrder: normalizeNumber(option.sortOrder) + } + }) + .filter(Boolean) + .sort((left, right) => ( + (left!.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right!.sortOrder ?? Number.MAX_SAFE_INTEGER) + || left!.channelCode.localeCompare(right!.channelCode) + )) as GuideStopAudioOption[] +) + const supportedLanguagesFromVariants = (variants: Record) => ( (Object.values(variants) as GuideStopLanguageVariant[]) .filter((variant) => variant.enabled && (variant.playable || variant.textAvailable)) .map((variant) => variant.lang) ) +const supportedLanguagesFromAudioOptions = (audioOptions: GuideStopAudioOption[]) => ( + Array.from(new Set(audioOptions.map((option) => option.languageCode))) +) + const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => { if (!value) return [] if (Array.isArray(value)) { @@ -337,7 +411,10 @@ export const toGuideStopInfo = ( ? normalizeSameOriginPublicUrl(source.coverImageUrl) || undefined : undefined const languageVariants = normalizeLanguageVariants(source.languageVariants) - const supportedLanguages = supportedLanguagesFromVariants(languageVariants) + const audioOptions = normalizeAudioOptions(source.audioOptions) + const supportedLanguages = audioOptions.length + ? supportedLanguagesFromAudioOptions(audioOptions) + : supportedLanguagesFromVariants(languageVariants) return { available: source.available === true, @@ -363,6 +440,7 @@ export const toGuideStopInfo = ( supportedLanguages: supportedLanguages.length ? supportedLanguages : normalizeSupportedLanguages(source.supportedLanguages), + audioOptions, languageVariants, audioStatus: source.audioStatus || 'MISSING', reason: source.reason || undefined, diff --git a/src/domain/museum.ts b/src/domain/museum.ts index fe021f7..f58d713 100644 --- a/src/domain/museum.ts +++ b/src/domain/museum.ts @@ -139,11 +139,27 @@ export interface MuseumHall { hasAudio?: boolean audioStatus?: string supportedLanguages?: string[] + audioOptionCount?: number area?: string poiId?: string location?: GuideLocationResolution } +export type MuseumAudioVoiceGender = 'female' | 'male' + +export interface MuseumAudioOption { + channelCode: string + displayName: string + languageCode: string + languageName?: string + gender: MuseumAudioVoiceGender + audioUrl: string + duration?: number + format?: string + isDefault: boolean + sortOrder?: number +} + export interface MuseumExhibit { id: string name: string @@ -179,6 +195,10 @@ export interface MuseumExhibit { audioAvailable?: boolean audioStatus?: string supportedLanguages?: string[] + audioOptions?: MuseumAudioOption[] + audioChannelCode?: string + audioVoiceGender?: MuseumAudioVoiceGender + audioVoiceDisplayName?: string audioVariants?: Record{{ audioDockTitle }} {{ audioDockSubtitle }} +