SARTAZAI/Step-Audio-EditX
0
1"""2Step-Audio-EditX - Audio Editing Demo using StepFun API3"""4import logging5import gradio as gr6 7from stepfun_api import get_api_token, process_audio # transcribe_audio8 9# Configure logging10logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')11logger = logging.getLogger(__name__)12 13 14def get_supported_edit_types():15 """16 获取支持的编辑类型和选项17 18 Returns:19 Dict[str, list]: Dictionary of edit types and their options20 """21 return {22 "clone": [],23 "emotion": [24 'happy', 'angry', 'sad', 'humour', 'confusion', 'disgusted',25 'empathy', 'embarrass', 'fear', 'surprised', 'excited',26 'depressed', 'coldness', 'admiration', 'remove'27 ],28 "style": [29 'serious', 'arrogant', 'child', 'older', 'girl', 'pure',30 'sister', 'sweet', 'ethereal', 'whisper', 'gentle', 'recite',31 'generous', 'act_coy', 'warm', 'shy', 'comfort', 'authority',32 'chat', 'radio', 'soulful', 'story', 'vivid', 'program',33 'news', 'advertising', 'roar', 'murmur', 'shout', 'deeply', 'loudly',34 'remove', 'exaggerated'35 ],36 "vad": [],37 "denoise": [],38 "paralinguistic": [],39 "speed": ["faster", "slower", "more faster", "more slower"],40 }41 42 43class EditxTab:44 """Audio editing and voice cloning interface tab"""45 46 def __init__(self):47 self.edit_type_list = list(get_supported_edit_types().keys())48 self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")49 50 def history_messages_to_show(self, messages):51 """Convert message history to gradio chatbot format"""52 show_msgs = []53 for message in messages:54 edit_type = message['edit_type']55 edit_info = message['edit_info']56 source_text = message['source_text']57 target_text = message['target_text']58 raw_audio_path = message['raw_audio_path']59 edit_audio_path = message['edit_audio_path']60 type_str = f"{edit_type}-{edit_info}" if edit_info is not None else f"{edit_type}"61 show_msgs.extend([62 {"role": "user", "content": f"任务类型:{type_str}\n文本:{source_text}"},63 {"role": "user", "content": gr.Audio(value=raw_audio_path, interactive=False)},64 {"role": "assistant", "content": f"输出音频:\n文本:{target_text}"},65 {"role": "assistant", "content": gr.Audio(value=edit_audio_path, interactive=False)}66 ])67 return show_msgs68 69 def generate_clone(self, prompt_text_input, prompt_audio_input, generated_text, edit_type, edit_info, state):70 """Generate cloned audio using API"""71 self.logger.info("Starting voice cloning via API")72 73 # Input validation74 if not prompt_text_input or prompt_text_input.strip() == "":75 error_msg = "[Error] Uploaded text cannot be empty."76 self.logger.error(error_msg)77 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state78 if not prompt_audio_input:79 error_msg = "[Error] Uploaded audio cannot be empty."80 self.logger.error(error_msg)81 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state82 if not generated_text or generated_text.strip() == "":83 error_msg = "[Error] Clone content cannot be empty."84 self.logger.error(error_msg)85 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state86 if edit_type != "clone":87 error_msg = "[Error] CLONE button must use clone task."88 self.logger.error(error_msg)89 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state90 91 try:92 # Call API93 output_path = process_audio(94 prompt_audio_input, prompt_text_input, generated_text, edit_type, edit_info95 )96 97 # Create message for history98 cur_msg = {99 "edit_type": edit_type,100 "edit_info": edit_info,101 "source_text": prompt_text_input,102 "target_text": generated_text,103 "raw_audio_path": prompt_audio_input,104 "edit_audio_path": output_path,105 }106 state["history_audio"].append((output_path, generated_text))107 state["history_messages"].append(cur_msg)108 109 show_msgs = self.history_messages_to_show(state["history_messages"])110 self.logger.info("Voice cloning completed successfully")111 return show_msgs, state112 113 except Exception as e:114 error_msg = f"[Error] Clone failed: {str(e)}"115 self.logger.error(error_msg)116 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state117 118 def generate_edit(self, prompt_text_input, prompt_audio_input, generated_text, edit_type, edit_info, state):119 """Generate edited audio using API"""120 self.logger.info("Starting audio editing via API")121 122 # Input validation123 if not prompt_audio_input:124 error_msg = "[Error] Uploaded audio cannot be empty."125 self.logger.error(error_msg)126 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state127 128 try:129 # Determine which audio to use130 if len(state["history_audio"]) == 0:131 audio_to_edit = prompt_audio_input132 text_to_use = prompt_text_input133 else:134 audio_to_edit, previous_text = state["history_audio"][-1]135 text_to_use = previous_text136 137 # For para-linguistic, use generated_text; otherwise use source text138 if edit_type not in {"paralinguistic"}:139 generated_text = text_to_use140 141 # Call API142 output_path = process_audio(143 audio_to_edit, text_to_use, generated_text, edit_type, edit_info144 )145 146 # Create message for history147 cur_msg = {148 "edit_type": edit_type,149 "edit_info": edit_info,150 "source_text": text_to_use,151 "target_text": generated_text,152 "raw_audio_path": audio_to_edit,153 "edit_audio_path": output_path,154 }155 state["history_audio"].append((output_path, generated_text))156 state["history_messages"].append(cur_msg)157 158 show_msgs = self.history_messages_to_show(state["history_messages"])159 self.logger.info("Audio editing completed successfully")160 return show_msgs, state161 162 except Exception as e:163 error_msg = f"[Error] Edit failed: {str(e)}"164 self.logger.error(error_msg)165 return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state166 167 def clear_history(self, state):168 """Clear conversation history"""169 state["history_messages"] = []170 state["history_audio"] = []171 return [], state172 173 # def auto_transcribe_audio(self, audio_path, state):174 # """175 # 自动转录音频文件,一次性返回最终结果176 177 # Args:178 # audio_path: 音频文件路径179 # state: 状态字典180 181 # Returns:182 # 转录的文本内容和更新后的状态183 # """184 # if not audio_path:185 # return "", state186 187 # # 防止重复调用 - 简化逻辑188 # if state.get("last_audio_path") == audio_path:189 # self.logger.debug(f"⚠️ Skipping duplicate transcription request for: {audio_path}")190 # return state.get("last_transcribed_text", ""), state191 192 # try:193 # # 更新音频路径194 # state["last_audio_path"] = audio_path195 196 # self.logger.info(f"🎙️ Starting auto transcription for: {audio_path}")197 198 # # 使用stepfun_api中的transcribe_audio函数,不使用streaming模式199 # transcribed_text = transcribe_audio(audio_path, streaming=False)200 201 # # 转录完成,缓存结果202 # state["last_transcribed_text"] = transcribed_text203 204 # self.logger.info(f"✅ Auto transcription completed: {transcribed_text}")205 # return transcribed_text, state206 207 # except Exception as e:208 # error_msg = f"[转录失败: {str(e)}]"209 # self.logger.error(f"❌ Auto transcription failed: {str(e)}")210 # state["last_transcribed_text"] = error_msg211 # return error_msg, state212 213 def init_state(self):214 """Initialize conversation state"""215 return {216 "history_messages": [],217 "history_audio": []218 # # ASR相关状态(已禁用)219 # "last_audio_path": None, # 用于防重复调用220 # "last_transcribed_text": "" # 缓存最后的转录结果221 }222 223 def update_edit_info(self, category):224 """Update sub-task dropdown based on main task selection"""225 category_items = get_supported_edit_types()226 choices = category_items.get(category, [])227 value = None if len(choices) == 0 else choices[0]228 return gr.Dropdown(label="Sub-task", choices=choices, value=value)229 230 def register_components(self):231 """Register gradio components - maintaining exact layout from original"""232 with gr.Tab("Editx"):233 with gr.Row():234 with gr.Column():235 self.model_input = gr.Textbox(label="Model Name", value="Step-Audio-EditX", scale=1)236 self.prompt_text_input = gr.Textbox(label="Prompt Text", value="", scale=1)237 self.prompt_audio_input = gr.Audio(238 sources=["upload", "microphone"],239 format="wav",240 type="filepath",241 label="Input Audio",242 )243 self.generated_text = gr.Textbox(label="Target Text", lines=1, max_lines=200, max_length=1000)244 with gr.Column():245 with gr.Row():246 self.edit_type = gr.Dropdown(label="Task", choices=self.edit_type_list, value="clone")247 self.edit_info = gr.Dropdown(label="Sub-task", choices=[], value=None)248 self.chat_box = gr.Chatbot(label="History", type="messages", height=480*1)249 with gr.Row():250 with gr.Column():251 with gr.Row():252 self.button_tts = gr.Button("CLONE", variant="primary")253 self.button_edit = gr.Button("EDIT", variant="primary")254 with gr.Column():255 self.clean_history_submit = gr.Button("Clear History", variant="primary")256 257 gr.Markdown("---")258 gr.Markdown("""259 **Button Description:**260 - CLONE: Synthesizes audio based on uploaded audio and text, only used for clone mode, will clear history information when used.261 - EDIT: Edits based on uploaded audio, or continues to stack edit effects based on the previous round of generated audio.262 """)263 gr.Markdown("""264 **Operation Workflow:**265 - Upload the audio to be edited on the left side and fill in the corresponding text content of the audio;266 - If the task requires modifying text content (such as clone, para-linguistic), fill in the text to be synthesized in the "target text" field. For all other tasks, keep the uploaded audio text content unchanged;267 - Select tasks and subtasks on the right side (some tasks have no subtasks, such as vad, etc.);268 - Click the "CLONE" or "EDIT" button on the left side, and audio will be generated in the dialog box on the right side.269 """)270 gr.Markdown("""271 **Para-linguistic Description:**272 - Supported tags include: [Breathing] [Laughter] [Surprise-oh] [Confirmation-en] [Uhm] [Surprise-ah] [Surprise-wa] [Sigh] [Question-ei] [Dissatisfaction-hnn]273 - Example:274 - Fill in "target text" field: "Great, the weather is so nice today." Click the "CLONE" button to get audio.275 - Change "target text" field to: "Great[Laughter], the weather is so nice today[Surprise-ah]." Click the "EDIT" button to get para-linguistic audio.276 """)277 278 def register_events(self):279 """Register event handlers"""280 state = gr.State(self.init_state())281 282 self.button_tts.click(283 self.generate_clone,284 inputs=[self.prompt_text_input, self.prompt_audio_input, self.generated_text, self.edit_type, self.edit_info, state],285 outputs=[self.chat_box, state]286 )287 self.button_edit.click(288 self.generate_edit,289 inputs=[self.prompt_text_input, self.prompt_audio_input, self.generated_text, self.edit_type, self.edit_info, state],290 outputs=[self.chat_box, state]291 )292 self.clean_history_submit.click(self.clear_history, inputs=[state], outputs=[self.chat_box, state])293 self.edit_type.change(294 fn=self.update_edit_info,295 inputs=self.edit_type,296 outputs=self.edit_info,297 )298 299 # # 音频上传时自动转录300 # self.prompt_audio_input.change(301 # fn=self.auto_transcribe_audio,302 # inputs=[self.prompt_audio_input, state],303 # outputs=[self.prompt_text_input, state]304 # )305 306 307def create_demo():308 """Create and return the Gradio demo"""309 editx_tab = EditxTab()310 311 with gr.Blocks(312 theme=gr.themes.Soft(),313 title="🎙️ Step-Audio-EditX",314 css="""315:root {316 --font: "Helvetica Neue", Helvetica, Arial, sans-serif;317 --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;318}319"""320 ) as demo:321 gr.Markdown("## 🎙️ Step-Audio-EditX")322 gr.Markdown("Audio Editing and Zero-Shot Cloning using Step-Audio-EditX")323 324 editx_tab.register_components()325 editx_tab.register_events()326 327 return demo328 329 330# Main entry point331if __name__ == "__main__":332 logger.info("🚀 Starting Step-Audio-EditX Demo (API Mode)")333 logger.info(f"API Token configured: {'Yes' if get_api_token() else 'No'}")334 335 demo = create_demo()336 demo.queue().launch(337 server_name="0.0.0.0",338 server_port=7860,339 share=False340 )341 