{"id":17531137,"url":"https://github.com/milosgajdos/playht_rs","last_synced_at":"2025-03-29T01:46:14.125Z","repository":{"id":232013237,"uuid":"780974651","full_name":"milosgajdos/playht_rs","owner":"milosgajdos","description":"PlayHT TTS Rust crate","archived":false,"fork":false,"pushed_at":"2024-04-14T07:47:32.000Z","size":67,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-14T10:58:35.956Z","etag":null,"topics":["ai","rust","rust-lang","speech-synthesis","text-to-speech","tts","tts-api"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/milosgajdos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-04-02T14:12:33.000Z","updated_at":"2024-04-16T14:49:25.423Z","dependencies_parsed_at":"2024-04-16T14:49:22.487Z","dependency_job_id":"3546583b-9e62-4f76-80d7-3a3c58ba3eec","html_url":"https://github.com/milosgajdos/playht_rs","commit_stats":null,"previous_names":["milosgajdos/playht_rs"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milosgajdos%2Fplayht_rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milosgajdos%2Fplayht_rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milosgajdos%2Fplayht_rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milosgajdos%2Fplayht_rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/milosgajdos","download_url":"https://codeload.github.com/milosgajdos/playht_rs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246126669,"owners_count":20727594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","rust","rust-lang","speech-synthesis","text-to-speech","tts","tts-api"],"created_at":"2024-10-20T17:22:58.714Z","updated_at":"2025-03-29T01:46:14.096Z","avatar_url":"https://github.com/milosgajdos.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# playht_rs\n\n[![Crates.io Version](https://img.shields.io/crates/v/playht_rs.svg)](https://crates.io/crates/playht_rs)\n[![Build Status](https://github.com/milosgajdos/playht_rs/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/milosgajdos/playht_rs/actions?query=workflow%3ACI)\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\nAn unofficial [play.ht](https://play.ht) Rust API client crate. Similar to the [Go module](https://github.com/milosgajdos/go-playht) implementation.\n\nIn order to use this create you must create an account on [play.ht](https://play.ht), generate an API secret and retrieve your User ID.\nSee the official docs [here](https://docs.play.ht/reference/api-authentication) for more info.\n\n# Basics\n\nThere are two ways to create audio/speech from the text using the API:\n\n- Job: audio generation is done in async; when you create a job you can monitor its progress via [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)\n- Stream: a real-time audio stream available immediately as soon as the stream has been created via the API\n\nThe API also allows you to clone a voice using a small sample of limited size. See the [docs](https://docs.play.ht/reference/api-create-instant-voice-clone).\n\n# Get started\n\n\u003e [!IMPORTANT]\n\u003e Before you attempt to run any of the samples you must set a couple of environment variables.\n\u003e These are automatically read by the client when it gets created; you can override them in your own code.\n\n- `PLAYHT_SECRET_KEY`: API secret key\n- `PLAYHT_USER_ID`: Play.HT User ID\n\nCheck the crate:\n\n```\ncargo check\n```\n\nBuild the crate:\n\n```shell\ncargo build\n```\n\n## Examples\n\nThere are quite a few examples available in the [examples](./examples) directory so please do have a look. They could give you some idea about how to use this crate. Below we list a few code samples:\n\n### Clone Voice\n\nClone a new voice from a sample audio file.\n\n\u003e [!NOTE]\n\u003e You must pass the sample file and the mime type as cli arguments\n\n```rust\n//! `cargo run --example clone_voices`\nuse playht_rs::{\n    api::{self, voice::CloneVoiceFileRequest, voice::DeleteClonedVoiceRequest},\n    prelude::*,\n};\nuse tokio;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n    let mut args = std::env::args().skip(1);\n    let sample_file = args.next().unwrap();\n    let mime_type = args.next().unwrap();\n\n    let req = CloneVoiceFileRequest {\n        sample_file,\n        mime_type,\n        voice_name: \"foo-bar\".to_owned(),\n    };\n\n    let client = api::Client::new();\n\n    let voice = client.clone_voice_from_file(req).await?;\n    println!(\"Got voice clone: {:?}\", voice);\n\n    let cloned_voices = client.get_cloned_voices().await?;\n    println!(\"Got voice clones: {:?}\", cloned_voices);\n\n    let req = DeleteClonedVoiceRequest { voice_id: voice.id };\n    let delete_resp = client.delete_cloned_voice(req).await?;\n    println!(\"Got delete response: {:?}\", delete_resp);\n\n    Ok(())\n}\n```\n\n### Create async TTS Jobs\n\nCreate an async TTS job and fetch its metadata.\n\n\u003e [!NOTE]\n\u003e The async TTS job progress can be monitored via the PlayHT API.\n\n```rust\n//! `cargo run --example tts_jobs`\nuse playht_rs::{\n    api::{self, job::TTSJobReq, tts::Quality},\n    prelude::*,\n};\nuse tokio;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n    let client = api::Client::new();\n    let voices = client.get_stock_voices().await?;\n    if voices.is_empty() {\n        return Err(\"No voices available\".into());\n    }\n\n    let req = TTSJobReq {\n        text: Some(\"What is life?\".to_owned()),\n        voice: Some(voices[0].id.clone()),\n        quality: Some(Quality::Low),\n        speed: Some(1.0),\n        sample_rate: Some(24000),\n        ..Default::default()\n    };\n\n    let tts_job = client.create_tts_job(req).await?;\n    println!(\"TTS job created: {:?}\", tts_job);\n\n    let tts_job = client.get_tts_job(tts_job.id).await?;\n    println!(\"Got TTS job: {:?}\", tts_job);\n\n    Ok(())\n}\n```\n\n### Stream TTS Audio\n\nStream TTS audio in real-time into a file.\nThe file is provided via a cli argument but you can pass async writer implementation such as an audio device tokio wrapper, etc.\n\n\u003e [!NOTE]\n\u003e You must pass the output file path as cli argument.\n\n```rust\n//! `cargo run --example tts_write_audio_stream -- \"foobar.mp3\"`\nuse playht_rs::{\n    api::{self, stream::TTSStreamReq, tts::Quality},\n    prelude::*,\n};\nuse tokio::{fs::File, io::BufWriter};\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n    let mut args = std::env::args().skip(1);\n    let file_path = args.next().unwrap();\n\n    let client = api::Client::new();\n    let voices = client.get_stock_voices().await?;\n    if voices.is_empty() {\n        return Err(\"No voices available\".into());\n    }\n\n    let req = TTSStreamReq {\n        text: Some(\"What is life?\".to_owned()),\n        voice: Some(voices[0].id.to_owned()),\n        quality: Some(Quality::Low),\n        speed: Some(1.0),\n        sample_rate: Some(24000),\n        ..Default::default()\n    };\n    let file = File::create(file_path.clone()).await?;\n    let mut w = BufWriter::new(file);\n    client.write_audio_stream(\u0026mut w, req).await?;\n    println!(\"Done streaming into {}\", file_path);\n\n    Ok(())\n}\n```\n\n### Play the TTS audio from a file\n\n```rust\n//! `cargo run --example play_audio -- \"/path/to/audio.mp3\"`\nuse rodio::{Decoder, OutputStream, Sink};\nuse std::{fs::File, io::BufReader};\n\nfn main() {\n    let mut args = std::env::args().skip(1);\n    let sound_file = args.next().unwrap();\n\n    let (_stream, stream_handle) = OutputStream::try_default().unwrap();\n    let file = BufReader::new(File::open(\u0026sound_file).unwrap());\n    let source = Decoder::new(file).unwrap();\n    let sink = Sink::try_new(\u0026stream_handle).unwrap();\n    sink.append(source);\n    sink.sleep_until_end();\n}\n```\n\n### Play TTS audio stream data\n\n\u003e [!NOTE]\n\u003e This does NOT actually do streaming playback!\n\u003e It feteches all the data into a buffer and then sends it\n\u003e for the playback. If you need a real-time playback stream\n\u003e check the `tts_stream_audio` example below.\n\n```rust\n//! `cargo run --example tts_play_audio_stream`\nuse playht_rs::{\n    api::{self, stream::TTSStreamReq, tts::Quality},\n    prelude::*,\n};\nuse rodio::{Decoder, OutputStream, Sink};\nuse std::io::Cursor;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n    let client = api::Client::new();\n    let voices = client.get_stock_voices().await?;\n    if voices.is_empty() {\n        return Err(\"No voices available\".into());\n    }\n\n    let req = TTSStreamReq {\n        text: Some(\"What is life?\".to_owned()),\n        voice: Some(voices[0].id.to_owned()),\n        quality: Some(Quality::Low),\n        speed: Some(1.0),\n        sample_rate: Some(24000),\n        ..Default::default()\n    };\n\n    let (_stream, stream_handle) = OutputStream::try_default().unwrap();\n    let sink = Sink::try_new(\u0026stream_handle).unwrap();\n\n    let mut buffer = Vec::new();\n    client.write_audio_stream(\u0026mut buffer, req).await?;\n\n    let source = Decoder::new(Cursor::new(buffer)).unwrap();\n    sink.append(source);\n    sink.sleep_until_end();\n\n    Ok(())\n}\n```\n\n### Stream TTS audio in real-time\n\n```rust\n//! ` cargo run --example tts_stream_audio`\nuse bytes::BytesMut;\nuse playht_rs::{\n    api::{self, stream::TTSStreamReq, tts::Quality},\n    prelude::*,\n};\nuse rodio::{Decoder, OutputStream, Sink};\nuse std::io::Cursor;\nuse tokio_stream::StreamExt;\n\n// NOTE: this might need to be adjusted\nconst BUFFER_SIZE: usize = 1024 * 10;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n    let client = api::Client::new();\n    let voices = client.get_stock_voices().await?;\n    if voices.is_empty() {\n        return Err(\"No voices available for playback\".into());\n    }\n    let client = api::Client::new();\n    let req = TTSStreamReq {\n        text: Some(\"What is life?\".to_owned()),\n        voice: Some(voices[0].id.to_owned()),\n        quality: Some(Quality::Low),\n        speed: Some(1.0),\n        sample_rate: Some(24000),\n        ..Default::default()\n    };\n\n    let (_stream, stream_handle) = OutputStream::try_default().unwrap();\n    let sink = Sink::try_new(\u0026stream_handle).unwrap();\n\n    let mut stream = client.stream_audio(req).await?;\n    let mut accumulated = BytesMut::new();\n\n    while let Some(res) = stream.next().await {\n        match res {\n            Ok(chunk) =\u003e {\n                accumulated.extend_from_slice(\u0026chunk);\n                // Check if there's enough data to attempt decoding\n                if accumulated.len() \u003e BUFFER_SIZE {\n                    let cursor = Cursor::new(accumulated.clone().freeze().to_vec());\n                    match Decoder::new(cursor) {\n                        Ok(source) =\u003e {\n                            sink.append(source);\n                            accumulated.clear(); // Clear the buffer on successful append\n                        }\n                        Err(e) =\u003e {\n                            eprintln!(\"Failed to decode received audio: {}\", e);\n                        }\n                    }\n                }\n            }\n            Err(err) =\u003e return Err(format!(\"Playback error: {}\", err).into()),\n        }\n    }\n\n    // Flush any remaining data at the end\n    if !accumulated.is_empty() {\n        let cursor = Cursor::new(accumulated.to_vec());\n        match Decoder::new(cursor) {\n            Ok(source) =\u003e sink.append(source),\n            Err(e) =\u003e println!(\"Remaining data could not be decoded: {}\", e),\n        }\n    }\n\n    sink.sleep_until_end();\n    Ok(())\n}\n```\n\n## Nix\n\nThere is a Nix flake vailable which lets you work on the Rust create in a nix shell.\n\nJust run the following command and you are in the business:\n\n```shell\nnix develop\n```\n\n# TODO\n\n- [ ] gRPC streaming\n- [ ] clean up the messy code\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilosgajdos%2Fplayht_rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmilosgajdos%2Fplayht_rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilosgajdos%2Fplayht_rs/lists"}