Skip to main content

elevenlabs_sdk/services/
forced_alignment.rs

1//! Forced alignment service for aligning text to audio.
2//!
3//! Provides a multipart endpoint that takes an audio file and text input,
4//! returning character-level alignment data.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use elevenlabs_sdk::{ClientConfig, ElevenLabsClient};
10//!
11//! # async fn example() -> elevenlabs_sdk::Result<()> {
12//! let config = ClientConfig::builder("your-api-key").build();
13//! let client = ElevenLabsClient::new(config)?;
14//!
15//! let audio = std::fs::read("sample.mp3").unwrap();
16//! let alignment = client.forced_alignment().create(&audio, "sample.mp3", "Hello world").await?;
17//! println!("Aligned {} characters", alignment.characters.len());
18//! # Ok(())
19//! # }
20//! ```
21
22use super::voices::{append_file_part, append_text_field, uuid_v4_simple};
23use crate::{client::ElevenLabsClient, error::Result, types::ForcedAlignmentResponse};
24
25/// Forced alignment service providing typed access to alignment endpoints.
26///
27/// Obtained via [`ElevenLabsClient::forced_alignment`].
28#[derive(Debug)]
29pub struct ForcedAlignmentService<'a> {
30    client: &'a ElevenLabsClient,
31}
32
33impl<'a> ForcedAlignmentService<'a> {
34    /// Creates a new `ForcedAlignmentService` bound to the given client.
35    pub(crate) const fn new(client: &'a ElevenLabsClient) -> Self {
36        Self { client }
37    }
38
39    /// Aligns text to an audio file and returns character-level alignment data.
40    ///
41    /// Calls `POST /v1/forced-alignment` with a multipart request containing
42    /// the audio file and the text to align.
43    ///
44    /// # Arguments
45    ///
46    /// * `audio_data` — Raw bytes of the audio file.
47    /// * `file_name` — File name for the audio (e.g. `"audio.mp3"`).
48    /// * `text` — The text to align against the audio.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if the API request fails.
53    pub async fn create(
54        &self,
55        audio_data: &[u8],
56        file_name: &str,
57        text: &str,
58    ) -> Result<ForcedAlignmentResponse> {
59        let boundary = uuid_v4_simple();
60        let mut body = Vec::new();
61
62        append_file_part(
63            &mut body,
64            &boundary,
65            "file",
66            file_name,
67            "application/octet-stream",
68            audio_data,
69        );
70        append_text_field(&mut body, &boundary, "text", text);
71
72        // Close the multipart body.
73        body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
74
75        let content_type = format!("multipart/form-data; boundary={boundary}");
76        self.client.post_multipart("/v1/forced-alignment", body, &content_type).await
77    }
78}
79
80// ---------------------------------------------------------------------------
81// Tests
82// ---------------------------------------------------------------------------
83
84#[cfg(test)]
85#[expect(clippy::unwrap_used, reason = "tests use unwrap")]
86mod tests {
87    use wiremock::{
88        Mock, MockServer, ResponseTemplate,
89        matchers::{header, method, path},
90    };
91
92    use crate::{ElevenLabsClient, config::ClientConfig};
93
94    #[tokio::test]
95    async fn create_returns_alignment() {
96        let mock_server = MockServer::start().await;
97
98        Mock::given(method("POST"))
99            .and(path("/v1/forced-alignment"))
100            .and(header("xi-api-key", "test-key"))
101            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
102                "characters": [
103                    {"text": "H", "start": 0.0, "end": 0.1},
104                    {"text": "e", "start": 0.1, "end": 0.2},
105                    {"text": "l", "start": 0.2, "end": 0.3}
106                ],
107                "words": [
108                    {"text": "Hel", "start": 0.0, "end": 0.3, "loss": 0.1}
109                ],
110                "loss": 0.5
111            })))
112            .mount(&mock_server)
113            .await;
114
115        let config = ClientConfig::builder("test-key").base_url(mock_server.uri()).build();
116        let client = ElevenLabsClient::new(config).unwrap();
117
118        let audio = b"fake-audio-data";
119        let result = client.forced_alignment().create(audio, "test.mp3", "Hello").await.unwrap();
120
121        assert_eq!(result.characters.len(), 3);
122    }
123}