|
|
@@ -6,16 +6,63 @@ import type {
|
|
|
SttStartOptions,
|
|
|
SttUpdateOptions,
|
|
|
SttEventMap,
|
|
|
+ ILanguageItem,
|
|
|
} from '../types'
|
|
|
|
|
|
+// STT API接口定义
|
|
|
+interface ApiSTTResponse {
|
|
|
+ tokenName?: string
|
|
|
+ taskId?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ApiSTTStartOptions {
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+ languages: ILanguageItem[]
|
|
|
+ token: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ApiSTTStopOptions {
|
|
|
+ taskId: string
|
|
|
+ token: string
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ApiSTTQueryOptions {
|
|
|
+ taskId: string
|
|
|
+ token: string
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ApiSTTUpdateOptions {
|
|
|
+ taskId: string
|
|
|
+ token: string
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+ data: any
|
|
|
+ updateMaskList: string[]
|
|
|
+}
|
|
|
+
|
|
|
+// 真实的API调用实现
|
|
|
export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements ISttManagerAdapter {
|
|
|
private _initialized = false
|
|
|
private _config?: SttManagerConfig
|
|
|
private _userId: string | number = ''
|
|
|
private _channel: string = ''
|
|
|
+ private _rtmManager?: any
|
|
|
+ private _option?: { token: string; taskId: string }
|
|
|
+ private _appId: string = ''
|
|
|
+ private _gatewayAddress = 'https://api.agora.io'
|
|
|
+ private _baseUrl = 'https://service.agora.io/toolbox-overseas'
|
|
|
|
|
|
- constructor() {
|
|
|
+ constructor(rtmManager?: any, appId?: string) {
|
|
|
super()
|
|
|
+ this._rtmManager = rtmManager
|
|
|
+ if (appId) {
|
|
|
+ this._appId = appId
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async init(config: SttManagerConfig): Promise<void> {
|
|
|
@@ -26,13 +73,22 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
throw new SttError('INVALID_CONFIG', 'Missing required configuration parameters')
|
|
|
}
|
|
|
|
|
|
+ if (!this._appId) {
|
|
|
+ throw new SttError('APP_ID_REQUIRED', 'App ID is required for STT operations')
|
|
|
+ }
|
|
|
+
|
|
|
this._userId = userId
|
|
|
this._channel = channel
|
|
|
this._config = config
|
|
|
|
|
|
- // 这里会调用RTM管理器进行初始化
|
|
|
- // 暂时模拟初始化过程
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
+ // 调用RTM管理器进行初始化
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.join({
|
|
|
+ channel,
|
|
|
+ userId: userId.toString(),
|
|
|
+ userName,
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
this._initialized = true
|
|
|
this.emit('initialized', { userId, channel })
|
|
|
@@ -57,15 +113,60 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // 模拟开始转录过程
|
|
|
this.emit('transcriptionStarting', { languages })
|
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 200))
|
|
|
+ // 获取锁
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.acquireLock()
|
|
|
+ }
|
|
|
|
|
|
- this.emit('transcriptionStarted', {
|
|
|
- taskId: `task-${Date.now()}`,
|
|
|
- languages,
|
|
|
- })
|
|
|
+ try {
|
|
|
+ // 获取token
|
|
|
+ const tokenData = await this._apiSTTAcquireToken({
|
|
|
+ channel: this._channel,
|
|
|
+ uid: this._userId,
|
|
|
+ })
|
|
|
+ const token = tokenData.tokenName
|
|
|
+
|
|
|
+ if (!token) {
|
|
|
+ throw new SttError('TOKEN_ERROR', 'Failed to acquire token')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始转录
|
|
|
+ const startResponse = await this._apiSTTStartTranscription({
|
|
|
+ uid: this._userId,
|
|
|
+ channel: this._channel,
|
|
|
+ languages,
|
|
|
+ token,
|
|
|
+ })
|
|
|
+
|
|
|
+ const { taskId } = startResponse
|
|
|
+ this._option = { token, taskId }
|
|
|
+
|
|
|
+ // 更新RTM元数据
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await Promise.all([
|
|
|
+ this._rtmManager.updateLanguages(languages),
|
|
|
+ this._rtmManager.updateSttData({
|
|
|
+ status: 'start',
|
|
|
+ taskId,
|
|
|
+ token,
|
|
|
+ startTime: Date.now(),
|
|
|
+ duration: 3600000, // 1小时
|
|
|
+ }),
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ this.emit('transcriptionStarted', {
|
|
|
+ taskId,
|
|
|
+ languages,
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ // 释放锁
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.releaseLock()
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
this.emit('error', error as Error)
|
|
|
throw error
|
|
|
@@ -80,12 +181,45 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ const { taskId, token } = this._option || {}
|
|
|
+ if (!taskId) {
|
|
|
+ throw new SttError('TASK_NOT_FOUND', 'No active transcription task found')
|
|
|
+ }
|
|
|
+ if (!token) {
|
|
|
+ throw new SttError('TOKEN_NOT_FOUND', 'Token not found')
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
this.emit('transcriptionStopping')
|
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
+ // 获取锁
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.acquireLock()
|
|
|
+ }
|
|
|
|
|
|
- this.emit('transcriptionStopped')
|
|
|
+ try {
|
|
|
+ // 停止转录
|
|
|
+ await this._apiSTTStopTranscription({
|
|
|
+ taskId,
|
|
|
+ token,
|
|
|
+ uid: this._userId,
|
|
|
+ channel: this._channel,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 更新RTM元数据
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.updateSttData({
|
|
|
+ status: 'end',
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ this.emit('transcriptionStopped')
|
|
|
+ } finally {
|
|
|
+ // 释放锁
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.releaseLock()
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
this.emit('error', error as Error)
|
|
|
throw error
|
|
|
@@ -100,11 +234,21 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- // 模拟查询转录结果
|
|
|
- return {
|
|
|
- status: 'completed',
|
|
|
- results: [],
|
|
|
+ const { taskId, token } = this._option || {}
|
|
|
+ if (!taskId) {
|
|
|
+ throw new SttError('TASK_NOT_FOUND', 'No active transcription task found')
|
|
|
}
|
|
|
+ if (!token) {
|
|
|
+ throw new SttError('TOKEN_NOT_FOUND', 'Token not found')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询转录结果
|
|
|
+ return await this._apiSTTQueryTranscription({
|
|
|
+ taskId,
|
|
|
+ token,
|
|
|
+ uid: this._userId,
|
|
|
+ channel: this._channel,
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
async updateTranscription(options: SttUpdateOptions): Promise<void> {
|
|
|
@@ -115,12 +259,28 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ const { taskId, token } = this._option || {}
|
|
|
+ if (!taskId) {
|
|
|
+ throw new SttError('TASK_NOT_FOUND', 'No active transcription task found')
|
|
|
+ }
|
|
|
+ if (!token) {
|
|
|
+ throw new SttError('TOKEN_NOT_FOUND', 'Token not found')
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
const { data, updateMaskList } = options
|
|
|
|
|
|
this.emit('transcriptionUpdating', { data, updateMaskList })
|
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
+ // 更新转录
|
|
|
+ await this._apiSTTUpdateTranscription({
|
|
|
+ taskId,
|
|
|
+ token,
|
|
|
+ uid: this._userId,
|
|
|
+ channel: this._channel,
|
|
|
+ data,
|
|
|
+ updateMaskList,
|
|
|
+ })
|
|
|
|
|
|
this.emit('transcriptionUpdated', { data })
|
|
|
} catch (error) {
|
|
|
@@ -140,7 +300,17 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
try {
|
|
|
this.emit('durationExtending', options)
|
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 50))
|
|
|
+ // 更新RTM元数据
|
|
|
+ if (this._rtmManager) {
|
|
|
+ const data: any = {}
|
|
|
+ if (options.startTime) {
|
|
|
+ data.startTime = options.startTime
|
|
|
+ }
|
|
|
+ if (options.duration) {
|
|
|
+ data.duration = options.duration
|
|
|
+ }
|
|
|
+ await this._rtmManager.updateSttData(data)
|
|
|
+ }
|
|
|
|
|
|
this.emit('durationExtended', options)
|
|
|
} catch (error) {
|
|
|
@@ -154,10 +324,15 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
this.emit('destroying')
|
|
|
|
|
|
// 清理资源
|
|
|
+ if (this._rtmManager) {
|
|
|
+ await this._rtmManager.destroy()
|
|
|
+ }
|
|
|
+
|
|
|
this._initialized = false
|
|
|
this._config = undefined
|
|
|
this._userId = ''
|
|
|
this._channel = ''
|
|
|
+ this._option = undefined
|
|
|
|
|
|
this.emit('destroyed')
|
|
|
|
|
|
@@ -183,4 +358,179 @@ export class SttManagerAdapter extends AGEventEmitter<SttEventMap> implements IS
|
|
|
get channel(): string {
|
|
|
return this._channel
|
|
|
}
|
|
|
+
|
|
|
+ // 私有API方法
|
|
|
+ private async _apiSTTAcquireToken(options: {
|
|
|
+ channel: string
|
|
|
+ uid: string | number
|
|
|
+ }): Promise<ApiSTTResponse> {
|
|
|
+ const { channel } = options
|
|
|
+ const data: any = {
|
|
|
+ instanceId: channel,
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = `${this._gatewayAddress}/v1/projects/${this._appId}/rtsc/speech-to-text/builderTokens`
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: await this._genAuthorization(options),
|
|
|
+ },
|
|
|
+ body: JSON.stringify(data),
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ return await response.json()
|
|
|
+ } else {
|
|
|
+ throw new Error(`API call failed with status: ${response.status}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _apiSTTStartTranscription(
|
|
|
+ options: ApiSTTStartOptions
|
|
|
+ ): Promise<{ taskId: string }> {
|
|
|
+ const { channel, languages, token, uid } = options
|
|
|
+ const url = `${this._gatewayAddress}/v1/projects/${this._appId}/rtsc/speech-to-text/tasks?builderToken=${token}`
|
|
|
+
|
|
|
+ const body: any = {
|
|
|
+ languages: languages.map((item) => item.source),
|
|
|
+ maxIdleTime: 60,
|
|
|
+ rtcConfig: {
|
|
|
+ channelName: channel,
|
|
|
+ subBotUid: '1000',
|
|
|
+ pubBotUid: '2000',
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ if (languages.find((item) => item.target?.length)) {
|
|
|
+ body.translateConfig = {
|
|
|
+ forceTranslateInterval: 2,
|
|
|
+ languages: languages.filter((item) => item.target?.length),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: await this._genAuthorization({
|
|
|
+ uid,
|
|
|
+ channel,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ body: JSON.stringify(body),
|
|
|
+ })
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+ if (response.status !== 200) {
|
|
|
+ throw new Error(data?.message || 'Start transcription failed')
|
|
|
+ }
|
|
|
+ return data
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _apiSTTStopTranscription(options: ApiSTTStopOptions): Promise<void> {
|
|
|
+ const { taskId, token, uid, channel } = options
|
|
|
+ const url = `${this._gatewayAddress}/v1/projects/${this._appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'DELETE',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: await this._genAuthorization({
|
|
|
+ uid,
|
|
|
+ channel,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Stop transcription failed with status: ${response.status}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _apiSTTQueryTranscription(options: ApiSTTQueryOptions): Promise<any> {
|
|
|
+ const { taskId, token, uid, channel } = options
|
|
|
+ const url = `${this._gatewayAddress}/v1/projects/${this._appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: await this._genAuthorization({
|
|
|
+ uid,
|
|
|
+ channel,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ return await response.json()
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _apiSTTUpdateTranscription(options: ApiSTTUpdateOptions): Promise<any> {
|
|
|
+ const { taskId, token, uid, channel, data, updateMaskList } = options
|
|
|
+ const updateMask = updateMaskList.join(',')
|
|
|
+ const url = `${this._gatewayAddress}/v1/projects/${this._appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}&sequenceId=1&updateMask=${updateMask}`
|
|
|
+
|
|
|
+ const body: any = {
|
|
|
+ ...data,
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'PATCH',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: await this._genAuthorization({
|
|
|
+ uid,
|
|
|
+ channel,
|
|
|
+ }),
|
|
|
+ },
|
|
|
+ body: JSON.stringify(body),
|
|
|
+ })
|
|
|
+
|
|
|
+ return await response.json()
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _genAuthorization(config: {
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+ }): Promise<string> {
|
|
|
+ // 在实际实现中,这里应该调用真实的token生成逻辑
|
|
|
+ // 这里简化实现,返回一个基本的认证头
|
|
|
+ const token = await this._apiGetAgoraToken(config)
|
|
|
+ return `agora token="${token}"`
|
|
|
+ }
|
|
|
+
|
|
|
+ private async _apiGetAgoraToken(config: {
|
|
|
+ uid: string | number
|
|
|
+ channel: string
|
|
|
+ }): Promise<string | null> {
|
|
|
+ const { uid, channel } = config
|
|
|
+ const url = `${this._baseUrl}/v2/token/generate`
|
|
|
+
|
|
|
+ const data = {
|
|
|
+ appId: this._appId,
|
|
|
+ appCertificate: '', // 在实际实现中需要提供证书
|
|
|
+ channelName: channel,
|
|
|
+ expire: 7200,
|
|
|
+ src: 'web',
|
|
|
+ types: [1, 2],
|
|
|
+ uid: uid.toString(),
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify(data),
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ const result = await response.json()
|
|
|
+ return result?.data?.token || null
|
|
|
+ }
|
|
|
+
|
|
|
+ return null
|
|
|
+ }
|
|
|
}
|