Spaces:
Running
Running
| """ | |
| Step-Audio-EditX - Audio Editing Demo using StepFun API | |
| """ | |
| import logging | |
| import gradio as gr | |
| from stepfun_api import get_api_token, process_audio, transcribe_audio | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| def get_supported_edit_types(): | |
| """ | |
| 获取支持的编辑类型和选项 | |
| Returns: | |
| Dict[str, list]: Dictionary of edit types and their options | |
| """ | |
| return { | |
| "clone": [], | |
| "emotion": [ | |
| 'happy', 'angry', 'sad', 'humour', 'confusion', 'disgusted', | |
| 'empathy', 'embarrass', 'fear', 'surprised', 'excited', | |
| 'depressed', 'coldness', 'admiration', 'remove' | |
| ], | |
| "style": [ | |
| 'serious', 'arrogant', 'child', 'older', 'girl', 'pure', | |
| 'sister', 'sweet', 'ethereal', 'whisper', 'gentle', 'recite', | |
| 'generous', 'act_coy', 'warm', 'shy', 'comfort', 'authority', | |
| 'chat', 'radio', 'soulful', 'story', 'vivid', 'program', | |
| 'news', 'advertising', 'roar', 'murmur', 'shout', 'deeply', 'loudly', | |
| 'remove', 'exaggerated' | |
| ], | |
| "vad": [], | |
| "denoise": [], | |
| "paralinguistic": [], | |
| "speed": ["faster", "slower", "more faster", "more slower"], | |
| } | |
| class EditxTab: | |
| """Audio editing and voice cloning interface tab""" | |
| def __init__(self): | |
| self.edit_type_list = list(get_supported_edit_types().keys()) | |
| self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") | |
| def history_messages_to_show(self, messages): | |
| """Convert message history to gradio chatbot format""" | |
| show_msgs = [] | |
| for message in messages: | |
| edit_type = message['edit_type'] | |
| edit_info = message['edit_info'] | |
| source_text = message['source_text'] | |
| target_text = message['target_text'] | |
| raw_audio_path = message['raw_audio_path'] | |
| edit_audio_path = message['edit_audio_path'] | |
| type_str = f"{edit_type}-{edit_info}" if edit_info is not None else f"{edit_type}" | |
| show_msgs.extend([ | |
| {"role": "user", "content": f"任务类型:{type_str}\n文本:{source_text}"}, | |
| {"role": "user", "content": gr.Audio(value=raw_audio_path, interactive=False)}, | |
| {"role": "assistant", "content": f"输出音频:\n文本:{target_text}"}, | |
| {"role": "assistant", "content": gr.Audio(value=edit_audio_path, interactive=False)} | |
| ]) | |
| return show_msgs | |
| def generate_clone(self, prompt_text_input, prompt_audio_input, generated_text, edit_type, edit_info, state): | |
| """Generate cloned audio using API""" | |
| self.logger.info("Starting voice cloning via API") | |
| # Input validation | |
| if not prompt_text_input or prompt_text_input.strip() == "": | |
| error_msg = "[Error] Uploaded text cannot be empty." | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| if not prompt_audio_input: | |
| error_msg = "[Error] Uploaded audio cannot be empty." | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| if not generated_text or generated_text.strip() == "": | |
| error_msg = "[Error] Clone content cannot be empty." | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| if edit_type != "clone": | |
| error_msg = "[Error] CLONE button must use clone task." | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| try: | |
| # Call API | |
| output_path = process_audio( | |
| prompt_audio_input, prompt_text_input, generated_text, edit_type, edit_info | |
| ) | |
| # Create message for history | |
| cur_msg = { | |
| "edit_type": edit_type, | |
| "edit_info": edit_info, | |
| "source_text": prompt_text_input, | |
| "target_text": generated_text, | |
| "raw_audio_path": prompt_audio_input, | |
| "edit_audio_path": output_path, | |
| } | |
| state["history_audio"].append((output_path, generated_text)) | |
| state["history_messages"].append(cur_msg) | |
| show_msgs = self.history_messages_to_show(state["history_messages"]) | |
| self.logger.info("Voice cloning completed successfully") | |
| return show_msgs, state | |
| except Exception as e: | |
| error_msg = f"[Error] Clone failed: {str(e)}" | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| def generate_edit(self, prompt_text_input, prompt_audio_input, generated_text, edit_type, edit_info, state): | |
| """Generate edited audio using API""" | |
| self.logger.info("Starting audio editing via API") | |
| # Input validation | |
| if not prompt_audio_input: | |
| error_msg = "[Error] Uploaded audio cannot be empty." | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| try: | |
| # Determine which audio to use | |
| if len(state["history_audio"]) == 0: | |
| audio_to_edit = prompt_audio_input | |
| text_to_use = prompt_text_input | |
| else: | |
| audio_to_edit, previous_text = state["history_audio"][-1] | |
| text_to_use = previous_text | |
| # For para-linguistic, use generated_text; otherwise use source text | |
| if edit_type not in {"paralinguistic"}: | |
| generated_text = text_to_use | |
| # Call API | |
| output_path = process_audio( | |
| audio_to_edit, text_to_use, generated_text, edit_type, edit_info | |
| ) | |
| # Create message for history | |
| cur_msg = { | |
| "edit_type": edit_type, | |
| "edit_info": edit_info, | |
| "source_text": text_to_use, | |
| "target_text": generated_text, | |
| "raw_audio_path": audio_to_edit, | |
| "edit_audio_path": output_path, | |
| } | |
| state["history_audio"].append((output_path, generated_text)) | |
| state["history_messages"].append(cur_msg) | |
| show_msgs = self.history_messages_to_show(state["history_messages"]) | |
| self.logger.info("Audio editing completed successfully") | |
| return show_msgs, state | |
| except Exception as e: | |
| error_msg = f"[Error] Edit failed: {str(e)}" | |
| self.logger.error(error_msg) | |
| return self.history_messages_to_show(state["history_messages"]) + [{"role": "user", "content": error_msg}], state | |
| def clear_history(self, state): | |
| """Clear conversation history""" | |
| state["history_messages"] = [] | |
| state["history_audio"] = [] | |
| return [], state | |
| def auto_transcribe_audio(self, audio_path, state): | |
| """ | |
| 自动转录音频文件,一次性返回最终结果 | |
| Args: | |
| audio_path: 音频文件路径 | |
| state: 状态字典 | |
| Returns: | |
| 转录的文本内容和更新后的状态 | |
| """ | |
| if not audio_path: | |
| return "", state | |
| # 防止重复调用 - 简化逻辑 | |
| if state.get("last_audio_path") == audio_path: | |
| self.logger.debug(f"⚠️ Skipping duplicate transcription request for: {audio_path}") | |
| return state.get("last_transcribed_text", ""), state | |
| try: | |
| # 更新音频路径 | |
| state["last_audio_path"] = audio_path | |
| self.logger.info(f"🎙️ Starting auto transcription for: {audio_path}") | |
| # 使用stepfun_api中的transcribe_audio函数,不使用streaming模式 | |
| transcribed_text = transcribe_audio(audio_path, streaming=False) | |
| # 转录完成,缓存结果 | |
| state["last_transcribed_text"] = transcribed_text | |
| self.logger.info(f"✅ Auto transcription completed: {transcribed_text}") | |
| return transcribed_text, state | |
| except Exception as e: | |
| error_msg = f"[转录失败: {str(e)}]" | |
| self.logger.error(f"❌ Auto transcription failed: {str(e)}") | |
| state["last_transcribed_text"] = error_msg | |
| return error_msg, state | |
| def init_state(self): | |
| """Initialize conversation state""" | |
| return { | |
| "history_messages": [], | |
| "history_audio": [], | |
| "last_audio_path": None, # 用于防重复调用 | |
| "last_transcribed_text": "" # 缓存最后的转录结果 | |
| } | |
| def update_edit_info(self, category): | |
| """Update sub-task dropdown based on main task selection""" | |
| category_items = get_supported_edit_types() | |
| choices = category_items.get(category, []) | |
| value = None if len(choices) == 0 else choices[0] | |
| return gr.Dropdown(label="Sub-task", choices=choices, value=value) | |
| def register_components(self): | |
| """Register gradio components - maintaining exact layout from original""" | |
| with gr.Tab("Editx"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| self.model_input = gr.Textbox(label="Model Name", value="Step-Audio-EditX", scale=1) | |
| self.prompt_text_input = gr.Textbox(label="Prompt Text", value="", scale=1) | |
| self.prompt_audio_input = gr.Audio( | |
| sources=["upload", "microphone"], | |
| format="wav", | |
| type="filepath", | |
| label="Input Audio", | |
| ) | |
| self.generated_text = gr.Textbox(label="Target Text", lines=1, max_lines=200, max_length=1000) | |
| with gr.Column(): | |
| with gr.Row(): | |
| self.edit_type = gr.Dropdown(label="Task", choices=self.edit_type_list, value="clone") | |
| self.edit_info = gr.Dropdown(label="Sub-task", choices=[], value=None) | |
| self.chat_box = gr.Chatbot(label="History", type="messages", height=480*1) | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Row(): | |
| self.button_tts = gr.Button("CLONE", variant="primary") | |
| self.button_edit = gr.Button("EDIT", variant="primary") | |
| with gr.Column(): | |
| self.clean_history_submit = gr.Button("Clear History", variant="primary") | |
| gr.Markdown("---") | |
| gr.Markdown(""" | |
| **Button Description:** | |
| - CLONE: Synthesizes audio based on uploaded audio and text, only used for clone mode, will clear history information when used. | |
| - EDIT: Edits based on uploaded audio, or continues to stack edit effects based on the previous round of generated audio. | |
| """) | |
| gr.Markdown(""" | |
| **Operation Workflow:** | |
| - Upload the audio to be edited on the left side and fill in the corresponding text content of the audio; | |
| - 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; | |
| - Select tasks and subtasks on the right side (some tasks have no subtasks, such as vad, etc.); | |
| - Click the "CLONE" or "EDIT" button on the left side, and audio will be generated in the dialog box on the right side. | |
| """) | |
| gr.Markdown(""" | |
| **Para-linguistic Description:** | |
| - Supported tags include: [Breathing] [Laughter] [Surprise-oh] [Confirmation-en] [Uhm] [Surprise-ah] [Surprise-wa] [Sigh] [Question-ei] [Dissatisfaction-hnn] | |
| - Example: | |
| - Fill in "target text" field: "Great, the weather is so nice today." Click the "CLONE" button to get audio. | |
| - Change "target text" field to: "Great[Laughter], the weather is so nice today[Surprise-ah]." Click the "EDIT" button to get para-linguistic audio. | |
| """) | |
| def register_events(self): | |
| """Register event handlers""" | |
| state = gr.State(self.init_state()) | |
| self.button_tts.click( | |
| self.generate_clone, | |
| inputs=[self.prompt_text_input, self.prompt_audio_input, self.generated_text, self.edit_type, self.edit_info, state], | |
| outputs=[self.chat_box, state] | |
| ) | |
| self.button_edit.click( | |
| self.generate_edit, | |
| inputs=[self.prompt_text_input, self.prompt_audio_input, self.generated_text, self.edit_type, self.edit_info, state], | |
| outputs=[self.chat_box, state] | |
| ) | |
| self.clean_history_submit.click(self.clear_history, inputs=[state], outputs=[self.chat_box, state]) | |
| self.edit_type.change( | |
| fn=self.update_edit_info, | |
| inputs=self.edit_type, | |
| outputs=self.edit_info, | |
| ) | |
| # 音频上传时自动转录 | |
| self.prompt_audio_input.change( | |
| fn=self.auto_transcribe_audio, | |
| inputs=[self.prompt_audio_input, state], | |
| outputs=[self.prompt_text_input, state] | |
| ) | |
| def create_demo(): | |
| """Create and return the Gradio demo""" | |
| editx_tab = EditxTab() | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(), | |
| title="🎙️ Step-Audio-EditX", | |
| css=""" | |
| :root { | |
| --font: "Helvetica Neue", Helvetica, Arial, sans-serif; | |
| --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; | |
| } | |
| """ | |
| ) as demo: | |
| gr.Markdown("## 🎙️ Step-Audio-EditX") | |
| gr.Markdown("Audio Editing and Zero-Shot Cloning using Step-Audio-EditX") | |
| editx_tab.register_components() | |
| editx_tab.register_events() | |
| return demo | |
| # Main entry point | |
| if __name__ == "__main__": | |
| logger.info("🚀 Starting Step-Audio-EditX Demo (API Mode)") | |
| logger.info(f"API Token configured: {'Yes' if get_api_token() else 'No'}") | |
| demo = create_demo() | |
| demo.queue().launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) | |