Create Text to Speech with Unlimited Length Using TTSFree API

Introduction

Text-to-speech (TTS) technology has revolutionized the way we interact with written content. Whether you’re developing a podcast, accessibility feature, or content presentation, having a robust text-to-speech solution is key. Many TTS services have limitations on the amount of text you can process at once, but what if there were a way to convert text of unlimited length into speech? Enter the TTSFree API—a powerful tool that allows you to generate speech from text with virtually no restrictions on length.

In this article, we will guide you on how to utilize the TTSFree API to create text-to-speech for long-form content seamlessly.

What is TTSFree API?

TTSFree API is a cloud-based service that converts text into natural-sounding speech. It supports various languages and voices, and it’s designed to be easy to integrate into any project. Unlike many other TTS services, TTSFree API offers flexibility in terms of the length of the text it can process. This makes it perfect for projects involving large amounts of text, such as books, long articles, or even scripts for video content.

 

ttsfree.com    limits text to 500 characters at a time. So we maka a script for Unlimited Length text and audio .So start

 

Step 1: Create the Database

We’ll start by creating two tables in the database:

  1. stories: Stores the full text and metadata.
  2. text_to_speech_tasks: Handles the chunked text and task status.

CREATE TABLE `stories` (
`story_id` int NOT NULL,
`story_title` varchar(255) NOT NULL,
`full_text` longtext NOT NULL,
`task_id` varchar(50) NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

 

CREATE TABLE `text_to_speech_tasks` (
`id` int NOT NULL,
`story_id` int NOT NULL,
`task_id` varchar(50) NOT NULL,
`chunk` text NOT NULL,
`status` enum(‘pending’,’processing’,’completed’,’failed’) DEFAULT ‘pending’,
`audio_path` varchar(255) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

These tables will allow us to store the original text, track the processing status, and store the resulting audio files.

 

Here is db.php

<?php
$conn = new mysqli(“localhost”, “db”, “pas”, “dbu”);
if ($conn->connect_error) {
die(“Connection failed: ” . $conn->connect_error);
}
?>

Step 2: Insert Large Text into Database

Create an index.html page to input and insert large text content into the stories table. This page should allow users to submit large text, which will be split and stored in the database.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Submit Multiple Stories</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <h1>Submit Multiple Stories</h1>
    <form id="storyForm">
        <div id="storyContainer">
            <div class="story">
                <label for="title-0">Story Title</label>
                <input id="title-0" type="text" name="stories[0][title]" placeholder="Story Title" required>
                <label for="text-0">Story Text</label>
                <textarea id="text-0" name="stories[0][text]" cols="30" rows="10" placeholder="Story Text (50-80k characters)" required></textarea>
            </div>
        </div>
        <button type="button" id="addStory">Add Another Story</button>
        <button type="submit">Submit Stories</button>
    </form>
    <div id="status"></div>

    <script>
        let storyCount = 1;
        const maxStories = 10;

        $("#addStory").on("click", function () {
            if (storyCount >= maxStories) {
                alert("You can only add up to " + maxStories + " stories.");
                return;
            }
            const storyHtml = `
                <div class="story">
                    <label for="title-${storyCount}">Story Title</label>
                    <input id="title-${storyCount}" type="text" name="stories[${storyCount}][title]" placeholder="Story Title" required>
                    <label for="text-${storyCount}">Story Text</label>
                    <textarea id="text-${storyCount}" name="stories[${storyCount}][text]" cols="30" rows="10" placeholder="Story Text (50-80k characters)" required></textarea>
                </div>
            `;
            $("#storyContainer").append(storyHtml);
            storyCount++;
        });

        $("#storyForm").on("submit", function (e) {
            e.preventDefault();
            $("#status").html("Submitting stories...");
            $.ajax({
                url: "submit_stories.php",
                type: "POST",
                data: $(this).serialize(),
                success: function (response) {
                    const data = JSON.parse(response);
                    if (data.success) {
                        $("#status").html("Stories submitted successfully!");
                    } else {
                        $("#status").html("Error submitting stories: " + data.message);
                    }
                },
                error: function () {
                    $("#status").html("An unexpected error occurred while submitting stories.");
                }
            });
        });
    </script>
</body>
</html>

Step 3: Split Text and Insert into Database (submit_stories.php)

Create a script called submit_stories.php to handle splitting the large text into chunks and inserting each chunk into the text_to_speech_tasks table.

<?php


include 'db.php'; // Include database connection

function split_text_into_chunks($text, $max_chunk_size = 490) {
    $chunks = [];
    $text_length = mb_strlen($text);
    $start = 0;

    while ($start < $text_length) {
        // Extract a chunk of maximum size
        $chunk = mb_substr($text, $start, $max_chunk_size);

        // Find the last period (.) in the chunk
        $last_dot_pos = mb_strrpos($chunk, '.');

        if ($last_dot_pos !== false && $last_dot_pos > 0) {
            // Cut the chunk at the last period
            $chunk = mb_substr($chunk, 0, $last_dot_pos + 1);
        }

        // Trim the chunk and add to the list
        $chunks[] = trim($chunk);

        // Move the start pointer forward by the length of the chunk
        $start += mb_strlen($chunk);
    }

    // Ensure the last chunk ends with a period
    $last_chunk_index = count($chunks) - 1;
    if ($last_chunk_index >= 0 && substr($chunks[$last_chunk_index], -1) !== '.') {
        $chunks[$last_chunk_index] .= '.';
    }

    return $chunks;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Validate input
    if (isset($_POST['stories']) && is_array($_POST['stories'])) {
        foreach ($_POST['stories'] as $index => $story) {
            $title = trim($story['title']);
            $text = trim($story['text']);

            // Ensure title and text are not empty
            if (empty($title) || empty($text)) {
                echo json_encode([
                    'success' => false,
                    'message' => "Story $index has an empty title or text."
                ]);
                exit;
            }

            // Ensure text does not exceed 80,000 characters
            if (mb_strlen($text) > 800000) {
                echo json_encode([
                    'success' => false,
                    'message' => "Story $index exceeds the 80,000 character limit."
                ]);
                exit;
            }

            // Generate a unique task ID
            $task_id = uniqid("task_");

            // Insert the story into the database
            $stmt = $conn->prepare("INSERT INTO stories (story_title, full_text, task_id) VALUES (?, ?, ?)");
            $stmt->bind_param("sss", $title, $text, $task_id);
            $stmt->execute();
            $story_id = $stmt->insert_id;

            // Split the text into chunks and store them in the database
            $text_chunks = split_text_into_chunks($text, 500);

            foreach ($text_chunks as $chunk) {
                $stmt = $conn->prepare("INSERT INTO text_to_speech_tasks (story_id, task_id, chunk, status) VALUES (?, ?, ?, 'pending')");
                $stmt->bind_param("iss", $story_id, $task_id, $chunk);
                $stmt->execute();
            }
        }

        // Return a success response
        echo json_encode([
            'success' => true,
            'message' => "All stories have been submitted successfully."
        ]);
    } else {
        echo json_encode([
            'success' => false,
            'message' => "No valid stories were submitted."
        ]);
    }
} else {
    echo json_encode([
        'success' => false,
        'message' => "Invalid request method."
    ]);
}

By this step we Split a Text File and Insert into Database

Step 4: Convert Chunks into Audio (process_chunks.php)

Now, create a script called process_chunks.php that will process each chunk of text and convert it into speech using the TTSFree API.

<?php


include 'db.php'; // Include the database connection

// Fetch one pending chunk
$result = $conn->query("SELECT id, task_id, chunk FROM text_to_speech_tasks WHERE status = 'pending' LIMIT 1");

if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $chunk_id = $row['id'];
        $task_id = $row['task_id'];
        $chunk_text = $row['chunk'];

        // Update status to 'processing'
        $conn->query("UPDATE text_to_speech_tasks SET status = 'processing' WHERE id = $chunk_id");

        // Call TTS API
        $post_data = json_encode([
            "voiceService" => "servicebin",
            "voiceID" => "",
            "voiceSpeed" => "0",
            "text" => $chunk_text,
        ]);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => "https://ttsfree.com/api/v1/tts",
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $post_data,
            CURLOPT_HTTPHEADER => [
                "apikey: ",
                "content-type: application/json",
            ],
        ]);

        $response = curl_exec($ch);
        $err = curl_error($ch);
        curl_close($ch);

        if ($err) {
            $conn->query("UPDATE text_to_speech_tasks SET status = 'failed' WHERE id = $chunk_id");
        } else {
            $data = json_decode($response, true);
            if (isset($data['audioData'])) {
                $audio_data = base64_decode($data['audioData']);
                $audio_path = "audio/task_$task_id" . "_chunk_$chunk_id.mp3";
                file_put_contents($audio_path, $audio_data);

                // Update status to 'completed'
                $stmt = $conn->prepare("UPDATE text_to_speech_tasks SET status = 'completed', audio_path = ? WHERE id = ?");
                $stmt->bind_param("si", $audio_path, $chunk_id);
                $stmt->execute();
            } else {
                $conn->query("UPDATE text_to_speech_tasks SET status = 'failed' WHERE id = $chunk_id");
            }
        }
    }
}

Step 5: Merge Audio Files (merge.php)

Create merge.php to merge all the audio files into a single file using FFmpeg.

<?php


include 'db.php';
include 'check_and_merge_audio.php'; // Include the merge function

// Fetch all stories that are not yet merged
$result = $conn->query("SELECT story_id FROM stories");

while ($row = $result->fetch_assoc()) {
    $story_id = $row['story_id'];
    check_and_merge_audio($story_id);
}
<?php


include 'db.php'; // Include database connection

function check_and_merge_audio($story_id) {
    global $conn;

    $audio_dir = "/www/wwwroot/audio";
    $file_list_path = "$audio_dir/file_list_$story_id.txt";

    // Fetch the story title
    $story_query = $conn->query("SELECT story_title FROM stories WHERE story_id = $story_id");
    $story = $story_query->fetch_assoc();

    if (!$story) {
        echo "Error: Story ID $story_id not found.\n";
        return;
    }

    // Sanitize the story title for use in a filename
    $story_title = preg_replace('/[^a-zA-Z0-9_-]/', '_', $story['story_title']);
    $final_audio_path = "$audio_dir/final_story_{$story_title}.mp3";

    $result = $conn->query("
        SELECT COUNT(*) AS total, 
               SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed
        FROM text_to_speech_tasks 
        WHERE story_id = $story_id
    ");
    $row = $result->fetch_assoc();

    if ($row['total'] == $row['completed'] && $row['total'] > 0) {
        echo "All chunks for story $story_id are completed. Proceeding with merge...\n";

        $file_list_path = generate_file_list($story_id, $conn);

        if ($file_list_path) {
            $command = "ffmpeg -f concat -safe 0 -i $file_list_path -c copy $final_audio_path 2>&1";
            exec($command, $output, $return_var);

            if ($return_var === 0) {
                echo "Audio merged successfully: $final_audio_path\n";
                $conn->query("DELETE FROM text_to_speech_tasks WHERE story_id = $story_id");
                $conn->query("DELETE FROM stories WHERE story_id = $story_id");
                unlink($file_list_path);
            } else {
                echo "Error merging audio files. FFmpeg output:\n" . implode("\n", $output) . "\n";
                file_put_contents(__DIR__ . "/merge_errors.log", "Story $story_id:\n" . implode("\n", $output) . "\n", FILE_APPEND);
            }
        }
    } else {
        echo "Story $story_id is not fully completed. Total: {$row['total']}, Completed: {$row['completed']}\n";
    }
}

function generate_file_list($story_id, $conn) {
    $audio_dir = "/www/wwwroot/audio";
    $file_list_path = "$audio_dir/file_list_$story_id.txt";

    $result = $conn->query("
        SELECT audio_path 
        FROM text_to_speech_tasks 
        WHERE story_id = $story_id AND status = 'completed' 
        ORDER BY id ASC
    ");

    if ($result->num_rows > 0) {
        $file_list_content = "";

        while ($row = $result->fetch_assoc()) {
            $relative_path = $row['audio_path'];
            $absolute_path = "/www/wwwroot/$relative_path";

            if (!file_exists($absolute_path)) {
                echo "Warning: Skipping missing file: $absolute_path\n";
                file_put_contents(__DIR__ . "/missing_files.log", "Missing file: $absolute_path\n", FILE_APPEND);
                continue;
            }

            $file_list_content .= "file '$absolute_path'\n";
        }

        if (file_put_contents($file_list_path, $file_list_content) !== false) {
            echo "File list created successfully: $file_list_path\n";
            return $file_list_path;
        } else {
            echo "Error: Unable to write file_list.txt to $file_list_path\n";
            return false;
        }
    } else {
        echo "No completed audio files found for story $story_id.\n";
        return false;
    }
}

Step 6: Set Cron Jobs

  1. Set a cron job to run process_chunks.php every minute to process the chunks.
  2. Set a cron job to run merge.php every hour to merge completed stories.

For example:

* * * * * php /path/to/process_chunks.php
0 * * * * php /path/to/merge.php

By following these steps, you’ll be able to process large amounts of text through TTSFree, convert them into speech, and merge the audio files into one seamless file for long-form content.

Leave a Reply

Your email address will not be published. Required fields are marked *