@@ -52,6 +52,11 @@ import {
5252 type AssemblyAIWord ,
5353 type AssemblyAIUtterance ,
5454} from '../utils/assemblyaiFileApi'
55+ import {
56+ transcribeFile as volcTranscribeFile ,
57+ type VolcWord ,
58+ type VolcUtterance ,
59+ } from '../utils/volcFileApi'
5560
5661/* ─── Soniox result conversion ──────────────────────────────── */
5762
@@ -802,6 +807,91 @@ async function executeAssemblyAI(
802807 return { transcript, tokens, segments, speakers, durationMs }
803808}
804809
810+ /* ─── Volcengine result conversion ──────────────────────────── */
811+
812+ function volcWordsToTokens ( words : VolcWord [ ] ) : TranscriptTokenData [ ] {
813+ return words . map ( ( w ) => ( {
814+ text : w . text ,
815+ isFinal : true as const ,
816+ startMs : w . start_time ,
817+ endMs : w . end_time ,
818+ confidence : w . confidence ,
819+ } ) )
820+ }
821+
822+ function volcUtterancesToSegments ( utterances : VolcUtterance [ ] ) : TranscriptSegment [ ] {
823+ return utterances . map ( ( u ) => ( {
824+ text : u . text ,
825+ startMs : u . start_time ,
826+ endMs : u . end_time ,
827+ isFinal : true as const ,
828+ speakerId : u . speaker ,
829+ } ) )
830+ }
831+
832+ function volcExtractSpeakers ( utterances : VolcUtterance [ ] ) : TranscriptSpeaker [ ] {
833+ const speakerSet = new Set < string > ( )
834+ for ( const u of utterances ) {
835+ if ( u . speaker ) speakerSet . add ( u . speaker )
836+ }
837+ return Array . from ( speakerSet )
838+ . sort ( )
839+ . map ( ( id ) => ( { id, label : `Speaker ${ id } ` } ) )
840+ }
841+
842+ async function executeVolc (
843+ file : File ,
844+ config : FileTranscriptionConfig ,
845+ appKey : string ,
846+ accessKey : string ,
847+ jobId : string ,
848+ updateJob : ( id : string , u : Record < string , unknown > ) => void ,
849+ signal : AbortSignal ,
850+ ) : Promise < TranscriptionResult > {
851+ updateJob ( jobId , { status : 'uploading' , progress : 20 } )
852+
853+ updateJob ( jobId , { status : 'transcribing' , progress : 40 } )
854+
855+ const response = await volcTranscribeFile (
856+ appKey ,
857+ accessKey ,
858+ file ,
859+ {
860+ enableSpeakerInfo : config . enableSpeakerDiarization ,
861+ enableItn : true ,
862+ enablePunc : true ,
863+ enableDdc : true ,
864+ } ,
865+ signal ,
866+ )
867+
868+ updateJob ( jobId , { progress : 90 } )
869+
870+ const transcript = response . result . text ?? ''
871+ const utterances = response . result . utterances ?? [ ]
872+ const durationMs = response . audio_info . duration ?? 0
873+
874+ console . debug ( '[Volcengine] Transcript length:' , transcript . length , 'Utterances:' , utterances . length , 'Duration:' , durationMs )
875+
876+ if ( ! transcript && utterances . length === 0 ) {
877+ console . warn ( '[Volcengine] Empty transcription result. Full response:' , JSON . stringify ( response ) )
878+ throw new Error ( '火山引擎返回了空的转录结果,请检查音频文件或尝试其他提供商。' )
879+ }
880+
881+ const allWords = utterances . flatMap ( ( u ) => u . words ?? [ ] )
882+ const tokens : TranscriptTokenData [ ] = allWords . length > 0
883+ ? volcWordsToTokens ( allWords )
884+ : [ { text : transcript , isFinal : true as const , startMs : 0 , endMs : durationMs } ]
885+
886+ const segments : TranscriptSegment [ ] = utterances . length > 0
887+ ? volcUtterancesToSegments ( utterances )
888+ : [ { text : transcript . trim ( ) , startMs : 0 , endMs : durationMs , isFinal : true as const } ]
889+
890+ const speakers = volcExtractSpeakers ( utterances )
891+
892+ return { transcript, tokens, segments, speakers, durationMs }
893+ }
894+
805895/* ─── Deepgram helpers ─────────────────────────────────────── */
806896
807897function parseDeepgramResponse ( response : import ( '../utils/deepgramFileApi' ) . DeepgramFileTranscriptionResponse ) {
@@ -915,6 +1005,12 @@ export function useFileTranscription() {
9151005 if ( ! apiToken || ! accountId ) {
9161006 throw new Error ( 'Cloudflare API Token 或 Account ID 未配置' )
9171007 }
1008+ } else if ( providerId === 'volc' ) {
1009+ const appKey = providerConfig ?. appKey as string | undefined
1010+ const accessKey = providerConfig ?. accessKey as string | undefined
1011+ if ( ! appKey || ! accessKey ) {
1012+ throw new Error ( '火山引擎 APP ID 或 Access Token 未配置' )
1013+ }
9181014 } else if ( ! apiKey ) {
9191015 throw new Error ( `${ providerId } API Key not configured` )
9201016 }
@@ -951,6 +1047,10 @@ export function useFileTranscription() {
9511047 result = await executeDeepgram ( file , config , apiKey ! , jobId , updateJob , controller . signal )
9521048 } else if ( providerId === 'assemblyai' ) {
9531049 result = await executeAssemblyAI ( file , config , apiKey ! , jobId , updateJob , controller . signal )
1050+ } else if ( providerId === 'volc' ) {
1051+ const volcAppKey = providerConfig ?. appKey as string
1052+ const volcAccessKey = providerConfig ?. accessKey as string
1053+ result = await executeVolc ( file , config , volcAppKey , volcAccessKey , jobId , updateJob , controller . signal )
9541054 } else {
9551055 result = await executeSoniox ( file , config , apiKey ! , jobId , updateJob , controller . signal )
9561056 }
0 commit comments