2626logger = logging .getLogger ("funasr.server" )
2727
2828
29+ def _split_text_for_openai_segments (text : str , max_chars : int = 80 ):
30+ """Split unsegmented ASR text into readable OpenAI-compatible cues."""
31+ text = text .strip ()
32+ if not text :
33+ return []
34+
35+ words = text .split ()
36+ if not words :
37+ return [text [i : i + max_chars ] for i in range (0 , len (text ), max_chars )]
38+
39+ parts = []
40+ current = []
41+ current_len = 0
42+ for word in words :
43+ next_len = len (word ) if not current else current_len + 1 + len (word )
44+ if current and next_len > max_chars :
45+ parts .append (" " .join (current ))
46+ current = []
47+ current_len = 0
48+
49+ current .append (word )
50+ current_len = len (word ) if current_len == 0 else current_len + 1 + len (word )
51+ if word [- 1 :] in ".!?;:" and current_len >= max_chars // 2 :
52+ parts .append (" " .join (current ))
53+ current = []
54+ current_len = 0
55+
56+ if current :
57+ parts .append (" " .join (current ))
58+
59+ if any (len (part ) > max_chars for part in parts ):
60+ return [text [i : i + max_chars ] for i in range (0 , len (text ), max_chars )]
61+
62+ return parts
63+
64+
65+ def build_openai_fallback_segments (text : str , duration : float , max_chars : int = 80 ):
66+ """Build coarse timestamped segments when a backend returns text only."""
67+ parts = _split_text_for_openai_segments (text , max_chars = max_chars )
68+ if not parts :
69+ return []
70+ if len (parts ) == 1 or duration <= 0 :
71+ return [{"start" : 0.0 , "end" : max (float (duration ), 0.0 ), "text" : parts [0 ]}]
72+
73+ total_chars = sum (len (part ) for part in parts )
74+ if total_chars <= 0 :
75+ return [{"start" : 0.0 , "end" : float (duration ), "text" : text .strip ()}]
76+
77+ segments = []
78+ consumed = 0
79+ previous_end = 0.0
80+ for i , part in enumerate (parts ):
81+ consumed += len (part )
82+ end = float (duration ) if i == len (parts ) - 1 else float (duration ) * consumed / total_chars
83+ end = max (end , previous_end )
84+ segments .append ({"start" : round (previous_end , 3 ), "end" : round (end , 3 ), "text" : part })
85+ previous_end = end
86+
87+ return segments
88+
89+
2990def prepare_audio_for_inference (audio_data , sr , target_sr = 16000 ):
3091 """Return mono float32 audio at target_sr for ASR inference."""
3192 audio_data = np .asarray (audio_data )
@@ -174,6 +235,10 @@ def _process_vllm(audio_data, sr, language=None, hotwords=None, use_spk=False):
174235 def _process_fallback (model_name , audio_path , language = None ):
175236 """Process with non-LLM model (SenseVoice/Paraformer)."""
176237 model = _load_fallback (model_name )
238+ try :
239+ duration = float (sf .info (audio_path ).duration )
240+ except Exception :
241+ duration = 0.0
177242 kwargs = {"input" : audio_path , "batch_size" : 1 }
178243 if language :
179244 kwargs ["language" ] = language
@@ -188,7 +253,9 @@ def _process_fallback(model_name, audio_path, language=None):
188253 "text" : re .sub (r'<\|[^|]*\|>' , '' , s .get ("text" , "" )).strip (),
189254 "speaker" : s .get ("spk" ),
190255 })
191- return {"text" : text , "segments" : segments }
256+ if not segments and text :
257+ segments = build_openai_fallback_segments (text , duration )
258+ return {"text" : text , "segments" : segments , "duration" : duration }
192259
193260 # Pre-load
194261 if preload_model == "fun-asr-nano" :
0 commit comments