Fish Speech-1.5多终端适配:H5网页嵌入、Android SDK、iOS语音播放集成
Fish Speech-1.5多终端适配H5网页嵌入、Android SDK、iOS语音播放集成Fish Speech-1.5是一个让人惊艳的文本转语音模型它基于超过100万小时的多语言音频数据训练而成能生成非常自然、富有表现力的语音。通过Xinference2.0.0部署后你可以在Web UI里轻松体验它的强大能力。但真正的价值在于如何把这个能力集成到你的实际项目中比如你想在自己的网站里加入语音播报功能或者在移动App里让虚拟助手开口说话。这篇文章我就来手把手带你实现Fish Speech-1.5在H5网页、Android和iOS三大终端上的集成让你能真正把这个技术用起来。1. 准备工作部署与基础调用在开始集成之前你得先有一个运行起来的Fish Speech-1.5服务。这里假设你已经通过Xinference完成了部署并且可以通过Web UI正常生成语音。1.1 确认服务状态首先确保你的模型服务已经成功启动。你可以通过查看日志来确认cat /root/workspace/model_server.log如果看到服务启动成功的相关日志就说明一切正常。初次加载模型可能需要一些时间请耐心等待。1.2 获取API访问信息终端集成需要通过API来调用模型。你需要知道服务的地址IP和端口。通常Xinference部署后会提供一个Web UI地址比如http://你的服务器IP:9997。这个地址的根路径就是你的API服务地址。为了测试API是否可用我们可以先用一个简单的curl命令试试水curl -X POST http://你的服务器IP:9997/v1/audio/speech \ -H Content-Type: application/json \ -d { model: fish-speech-1.5, input: 你好世界这是一段测试语音。, voice: default, language: zh } \ --output test_audio.wav如果命令执行成功并且生成了一个test_audio.wav文件用播放器打开能听到清晰的“你好世界”那么恭喜你API调用这条路就通了。这是后续所有终端集成的基础。2. H5网页嵌入实战现在我们来看看怎么在网页里使用这个语音合成能力。想象一下你有一个新闻网站想让用户能“听”新闻或者有一个教育平台需要语音朗读题目。H5集成是最直接的方式。2.1 前端调用API核心思路是网页上的JavaScript代码去调用我们刚才测试过的那个API。下面是一个最基础的HTML示例包含一个输入框、一个按钮和一个音频播放器。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleFish Speech H5 语音合成演示/title /head body h2网页语音合成演示/h2 textarea idtextInput rows4 cols50 placeholder请输入要合成的文本...欢迎使用Fish Speech语音合成服务。/textarea brbr button onclicksynthesizeSpeech()生成语音/button brbr audio idaudioPlayer controls/audio script // 替换成你的实际服务器地址 const API_BASE_URL http://你的服务器IP:9997/v1; async function synthesizeSpeech() { const text document.getElementById(textInput).value; const audioPlayer document.getElementById(audioPlayer); if (!text.trim()) { alert(请输入文本); return; } // 显示加载状态 audioPlayer.src ; const button event.target; button.textContent 生成中...; button.disabled true; try { const response await fetch(${API_BASE_URL}/audio/speech, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ model: fish-speech-1.5, input: text, voice: default, // 可根据需要选择音色 language: zh }) }); if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } // 获取音频Blob数据 const audioBlob await response.blob(); // 创建本地播放URL const audioUrl URL.createObjectURL(audioBlob); audioPlayer.src audioUrl; // 可选自动播放注意浏览器自动播放策略 // audioPlayer.play().catch(e console.log(自动播放被阻止:, e)); } catch (error) { console.error(语音合成失败:, error); alert(语音合成失败请检查控制台或网络。); } finally { // 恢复按钮状态 button.textContent 生成语音; button.disabled false; } } /script /body /html把这段代码保存为index.html用浏览器打开填入你的服务器IP就可以在网页里直接生成和播放语音了。2.2 处理跨域与优化体验在实际项目中你可能会遇到跨域问题。因为你的网页域名和API服务器域名不同浏览器出于安全考虑会阻止请求。解决方法是在部署Xinference的服务端配置CORS跨域资源共享。此外为了更好的用户体验你还可以添加加载动画在生成语音时显示一个旋转的加载图标。错误处理更优雅地提示网络错误、服务器错误或合成失败。参数扩展让用户可以选择语速、音调、不同音色等如果模型支持。兼容性处理确保代码在不同浏览器上都能正常工作。3. Android SDK集成指南对于Android应用比如阅读类App、语音助手、教育软件等集成语音合成功能能极大提升用户体验。我们通过封装网络请求可以轻松地将Fish Speech-1.5的能力接入Android App。3.1 核心网络请求封装首先在Android项目中你需要添加网络请求库的依赖比如Retrofit它是处理HTTP请求的利器。在app/build.gradle文件中添加dependencies { implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 // 用于JSON解析 // 如果你需要处理更复杂的响应如直接获取二进制音频流可以添加scalars转换器 // implementation com.squareup.retrofit2:converter-scalars:2.9.0 }然后我们定义一个数据模型和API接口// SpeechRequest.kt data class SpeechRequest( val model: String fish-speech-1.5, val input: String, val voice: String default, val language: String zh // 可以添加更多参数如 speed, pitch 等 ) // FishSpeechApiService.kt import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST interface FishSpeechApiService { POST(audio/speech) // 相对路径基础URL在Retrofit构建器中设置 fun synthesizeSpeech(Body request: SpeechRequest): CallResponseBody // 直接返回二进制流 }接下来创建一个管理类来处理语音合成// FishSpeechManager.kt import android.content.Context import android.media.MediaPlayer import android.util.Log import okhttp3.OkHttpClient import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.File import java.io.FileOutputStream import java.util.concurrent.TimeUnit class FishSpeechManager private constructor(context: Context) { companion object { private var instance: FishSpeechManager? null fun getInstance(context: Context): FishSpeechManager { return instance ?: synchronized(this) { instance ?: FishSpeechManager(context.applicationContext).also { instance it } } } } private val appContext: Context context private val apiService: FishSpeechApiService private var mediaPlayer: MediaPlayer? null private var currentAudioFile: File? null init { val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) // 合成可能需要时间设置长一点 .readTimeout(60, TimeUnit.SECONDS) .build() val retrofit Retrofit.Builder() .baseUrl(http://你的服务器IP:9997/v1/) // 设置基础URL .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() apiService retrofit.create(FishSpeechApiService::class.java) } interface SynthesisCallback { fun onSuccess(audioFile: File) fun onFailure(errorMessage: String) fun onProgress(progress: Int) // 可用于显示进度但简单API可能不支持 } fun synthesizeText(text: String, callback: SynthesisCallback) { if (text.isBlank()) { callback.onFailure(输入文本为空) return } val request SpeechRequest(input text) val call apiService.synthesizeSpeech(request) call.enqueue(object : CallbackResponseBody { override fun onResponse(call: CallResponseBody, response: ResponseResponseBody) { if (response.isSuccessful response.body() ! null) { // 将音频流保存到文件 val body response.body()!! try { // 创建临时文件存储音频 val tempFile File.createTempFile(fish_speech_, .wav, appContext.cacheDir) val inputStream body.byteStream() val outputStream FileOutputStream(tempFile) inputStream.copyTo(outputStream) outputStream.close() inputStream.close() currentAudioFile tempFile callback.onSuccess(tempFile) } catch (e: Exception) { Log.e(FishSpeechManager, 保存音频文件失败, e) callback.onFailure(保存音频失败: ${e.message}) } } else { val errorMsg 请求失败: ${response.code()} - ${response.message()} Log.e(FishSpeechManager, errorMsg) callback.onFailure(errorMsg) } } override fun onFailure(call: CallResponseBody, t: Throwable) { Log.e(FishSpeechManager, 网络请求失败, t) callback.onFailure(网络请求失败: ${t.message}) } }) } fun playAudio(file: File) { stopPlayback() // 停止当前播放 mediaPlayer MediaPlayer().apply { setDataSource(file.path) prepareAsync() setOnPreparedListener { it.start() } setOnCompletionListener { stopPlayback() } setOnErrorListener { mp, what, extra - Log.e(FishSpeechManager, 播放错误: what$what, extra$extra) false } } } fun stopPlayback() { mediaPlayer?.release() mediaPlayer null } fun cleanup() { stopPlayback() currentAudioFile?.delete() // 清理临时文件 currentAudioFile null } }3.2 在Activity中使用在Android的Activity或Fragment中你可以这样调用// MainActivity.kt 示例 import android.os.Bundle import android.widget.Button import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import java.io.File class MainActivity : AppCompatActivity() { private lateinit var editText: EditText private lateinit var synthButton: Button private lateinit var playButton: Button private lateinit var fishSpeechManager: FishSpeechManager private var currentAudioFile: File? null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) editText findViewById(R.id.editText) synthButton findViewById(R.id.synthButton) playButton findViewById(R.id.playButton) playButton.isEnabled false fishSpeechManager FishSpeechManager.getInstance(applicationContext) synthButton.setOnClickListener { val text editText.text.toString() synthButton.isEnabled false synthButton.text 合成中... fishSpeechManager.synthesizeText(text, object : FishSpeechManager.SynthesisCallback { override fun onSuccess(audioFile: File) { runOnUiThread { synthButton.isEnabled true synthButton.text 合成语音 currentAudioFile audioFile playButton.isEnabled true Toast.makeText(thisMainActivity, 语音合成成功, Toast.LENGTH_SHORT).show() } } override fun onFailure(errorMessage: String) { runOnUiThread { synthButton.isEnabled true synthButton.text 合成语音 Toast.makeText(thisMainActivity, 失败: $errorMessage, Toast.LENGTH_LONG).show() } } override fun onProgress(progress: Int) { // 如果API支持进度回调可以在这里更新UI } }) } playButton.setOnClickListener { currentAudioFile?.let { file - fishSpeechManager.playAudio(file) } ?: run { Toast.makeText(this, 请先合成语音, Toast.LENGTH_SHORT).show() } } } override fun onDestroy() { super.onDestroy() fishSpeechManager.cleanup() } }记得在AndroidManifest.xml中添加网络权限uses-permission android:nameandroid.permission.INTERNET /这样一个基本的Android语音合成功能就完成了。你可以根据需求增加音色选择、语速调节、播放列表管理等功能。4. iOS语音播放集成在iOS平台上无论是SwiftUI还是UIKit项目集成思路类似网络请求获取音频数据然后用系统框架播放。这里以Swift语言为例。4.1 网络请求与音频播放首先创建一个管理网络请求和音频播放的类// FishSpeechService.swift import Foundation import AVFoundation class FishSpeechService: NSObject, AVAudioPlayerDelegate { static let shared FishSpeechService() private override init() {} private var audioPlayer: AVAudioPlayer? private let baseURL http://你的服务器IP:9997/v1 // 替换为你的地址 typealias SynthesisCompletion (ResultURL, Error) - Void func synthesizeSpeech(text: String, completion: escaping SynthesisCompletion) { guard !text.isEmpty else { completion(.failure(NSError(domain: FishSpeech, code: -1, userInfo: [NSLocalizedDescriptionKey: 输入文本为空]))) return } let url URL(string: \(baseURL)/audio/speech)! var request URLRequest(url: url) request.httpMethod POST request.setValue(application/json, forHTTPHeaderField: Content-Type) let requestBody: [String: Any] [ model: fish-speech-1.5, input: text, voice: default, language: zh ] do { request.httpBody try JSONSerialization.data(withJSONObject: requestBody) } catch { completion(.failure(error)) return } let task URLSession.shared.dataTask(with: request) { [weak self] data, response, error in guard let self self else { return } if let error error { DispatchQueue.main.async { completion(.failure(error)) } return } guard let httpResponse response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode), let audioData data else { let statusCode (response as? HTTPURLResponse)?.statusCode ?? -1 let error NSError(domain: FishSpeech, code: statusCode, userInfo: [NSLocalizedDescriptionKey: 服务器返回错误: \(statusCode)]) DispatchQueue.main.async { completion(.failure(error)) } return } // 将音频数据保存到临时文件 do { let tempDir FileManager.default.temporaryDirectory let tempFileURL tempDir.appendingPathComponent(UUID().uuidString).appendingPathExtension(wav) try audioData.write(to: tempFileURL) DispatchQueue.main.async { completion(.success(tempFileURL)) } } catch { DispatchQueue.main.async { completion(.failure(error)) } } } task.resume() } func playAudio(from fileURL: URL) throws { // 停止当前播放 stopPlayback() audioPlayer try AVAudioPlayer(contentsOf: fileURL) audioPlayer?.delegate self audioPlayer?.prepareToPlay() audioPlayer?.play() } func stopPlayback() { audioPlayer?.stop() audioPlayer nil } var isPlaying: Bool { return audioPlayer?.isPlaying ?? false } // MARK: - AVAudioPlayerDelegate func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { stopPlayback() // 可以在这里发送播放完成的通知 } func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { stopPlayback() print(音频解码错误: \(error?.localizedDescription ?? 未知错误)) } }4.2 在SwiftUI视图中使用下面是一个简单的SwiftUI视图演示如何调用上述服务// ContentView.swift import SwiftUI struct ContentView: View { State private var inputText: String 欢迎使用Fish Speech语音合成。 State private var isSynthesizing: Bool false State private var currentAudioURL: URL? State private var errorMessage: String? State private var showError: Bool false var body: some View { VStack(spacing: 20) { Text(Fish Speech iOS 演示) .font(.largeTitle) .padding() TextEditor(text: $inputText) .frame(height: 150) .padding(4) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.gray.opacity(0.5), lineWidth: 1) ) .padding(.horizontal) Button(action: synthesizeSpeech) { HStack { if isSynthesizing { ProgressView() .scaleEffect(0.8) Text(合成中...) } else { Image(systemName: waveform) Text(合成语音) } } .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) } .disabled(isSynthesizing || inputText.isEmpty) .padding(.horizontal) if let url currentAudioURL { HStack(spacing: 20) { Button(action: playAudio) { HStack { Image(systemName: FishSpeechService.shared.isPlaying ? pause.circle.fill : play.circle.fill) Text(FishSpeechService.shared.isPlaying ? 播放中 : 播放) } .padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) } Button(action: stopAudio) { HStack { Image(systemName: stop.circle.fill) Text(停止) } .padding() .background(Color.red) .foregroundColor(.white) .cornerRadius(10) } } } Spacer() } .padding() .alert(错误, isPresented: $showError, presenting: errorMessage) { _ in Button(确定, role: .cancel) { } } message: { message in Text(message) } } private func synthesizeSpeech() { guard !inputText.isEmpty else { return } isSynthesizing true errorMessage nil FishSpeechService.shared.synthesizeSpeech(text: inputText) { result in isSynthesizing false switch result { case .success(let url): self.currentAudioURL url print(音频文件已保存至: \(url.path)) case .failure(let error): self.errorMessage error.localizedDescription self.showError true print(合成失败: \(error)) } } } private func playAudio() { guard let url currentAudioURL else { return } do { try FishSpeechService.shared.playAudio(from: url) } catch { errorMessage 播放失败: \(error.localizedDescription) showError true } } private func stopAudio() { FishSpeechService.shared.stopPlayback() } }重要提示在iOS中你需要配置App Transport Security (ATS) 以允许HTTP请求如果你的服务器没有使用HTTPS。在Info.plist中添加keyNSAppTransportSecurity/key dict keyNSAllowsArbitraryLoads/key true/ /dict对于生产环境强烈建议使用HTTPS并配置正确的ATS例外。5. 总结与进阶建议通过上面的步骤你应该已经成功将Fish Speech-1.5的语音合成能力集成到了H5网页、Android和iOS应用中。我们来回顾一下关键点并看看还能如何做得更好。5.1 核心步骤回顾服务部署与验证确保通过Xinference部署的Fish Speech-1.5 API可以正常访问和调用。H5网页集成使用JavaScript的Fetch API调用后端服务获取音频流并通过audio标签播放。核心是处理跨域和用户体验。Android集成使用Retrofit等库封装网络请求将音频数据保存为临时文件然后通过Android的MediaPlayer进行播放。需要注意生命周期管理和资源释放。iOS集成使用URLSession进行网络请求将音频数据保存到临时目录然后通过AVAudioPlayer框架播放。需要处理ATS安全策略。5.2 进阶优化建议当你完成了基础集成后可以考虑以下方向来提升应用的稳定性和用户体验网络优化超时与重试为网络请求设置合理的超时时间并实现失败重试机制。离线缓存对于合成过的文本可以将音频文件缓存到本地下次直接播放节省流量和等待时间。断点续传如果合成很长的文本可以考虑支持断点续传但这需要后端API支持。播放功能增强播放控制实现播放、暂停、停止、快进、快退等完整控制。播放列表支持多个语音片段的队列播放。后台播放在iOS和Android上配置后台播放权限让语音在App退到后台时也能继续适用于听书类应用。性能与稳定性内存管理及时释放不再使用的音频文件和播放器资源防止内存泄漏。错误监控收集合成失败、播放错误等日志便于排查问题。多音色与参数如果Fish Speech模型支持可以开放更多参数如语速、音高、不同说话人供用户选择。安全与合规使用HTTPS在生产环境中务必为你的API服务配置HTTPS以保证数据传输安全。鉴权与限流为API接口添加简单的Token鉴权防止被滥用。可以设置调用频率限制。把一项强大的AI能力从演示页面搬到真实可用的产品里中间需要这些扎实的工程化工作。希望这份指南能帮你跨过这道坎顺利在你的网页或App中响起Fish Speech合成的、自然流畅的语音。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。