Build Your Own News Radio Bot with Spitch, Telegram, and Langchain

Simeon AdebolaSimeon Adebola
10 min read

Introduction

In this tutorial, you'll discover how to create a personalized news retrieval bot that fetches the latest news and compiles it into a radio-style report. To get started, ensure you have the following:

  • python version 3.8 or higher

  • an api key from the spitch portal (you will need to create an account, if you don't already have one)

  • a telegram bot token from botfather( for more information, check this guide)

  • access to any langchain-compatible llm provider i.e. google-generative-ai, openai, anthropic or ollama e.t.c.

Once you have these prerequisites, you're ready to begin building your news radio bot.

Step 1: Environment setup

In your project folder create a requirements.txt file and place the below packages in it

beautifulsoup4==4.12.3
duckduckgo-search==7.5.2
langchain==0.3.23
langchain_community==0.3.21
python-telegram-bot==22.3
requests==2.32.3
spitch==1.33.0

Then run the below command in your terminal to install the packages

pip install -r requirements.txt
đź’ˇ
You also have to install the package of your langchain-compatible llm provider i.e. langchain-openai or langchain-google-genai. Additionally, you must have access to that specific provider through an api key.

Next, create a file named .env , in here you will store your; telegram, spitch and llm provider(in my case google-generative-ai ) api keys.

GOOGLE_API_KEY = "YOUR_GOOGLE_API_KEY" #  This depends on your langchain-compatible llm provider
SPITCH_API_KEY = "YOUR_SPITCH_API_KEY"
TELEBOT_TOKEN = "YOUR_TELEBOT_TOKEN"

Step 2: Create your chatbot

Create a file named chatbot.py and import the below packages

  # chatbot.py

  import logging

  from telegram import (
      Update,
      InlineKeyboardButton,
      ReplyKeyboardMarkup
  )

  from telegram.ext import (
      Application,
      ContextTypes,
      ConversationHandler,
      CommandHandler,
      MessageHandler, 
      CallbackQueryHandler,

  )

Logging is used to track the state of the requests coming to our bot application, while telegram and telegram.ext modules are used to create and handle our entire conversation workflow. Now we can create a logger instance

# chatbot.py
# ...existing code

# Set up logging early to see messages
logging.basicConfig(
      format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
  )
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
# create logger instance
logger = logging.getLogger(__name__)

Next we will define our states and state handlers.

# chatbot.py
# ...existing code

CATEGORY_HANDLER, FOLLOWUP_HANDLER, REPORT_HANDLER = range(3)

async def start(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Begins the conversation flow, and prompts the user to 
    provide a news category.
    """
    pass


async def handle_category_callback(update:Update, context:ContextTypes.DEFAULT_TYPE):
     """
    Retreives news articles based on user's chosen category.

    Generates summary of the retrieved news, and prompts the user 
    to choose between a radio report or not.
    """ 
    pass


async def handle_follow_up(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Based on the user's decision, dynamically chooses whether 
    to end the chat interaction or follow up on the radio report.
    """
    pass


async def respond_with_audio(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Returns an audio file based on the user's selected language.
    """
    pass


async def end(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Ends conversation gracefully in the case of a user quitting.
    """
    pass

You might noticed that we haven’t exactly placed any concrete logic into the functions, save for a few pass statements. We will fix this up in our next step.

Finally we can build our workflow

# Build workflow
def main():
    """
    Builds the conversation flow based on;
    - a bot token from https://telegram.me/BotFather
    - the defined states
    - the state handlers
    """
    application = Application.builder().token(os.getenv('TELEBOT_TOKEN')).build()

    # Create conversation handler
    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states= {
            CATEGORY_HANDLER : [CallbackQueryHandler(handle_category_callback)],
            FOLLOWUP_HANDLER : [CallbackQueryHandler(handle_follow_up)],
            REPORT_HANDLER : [CallbackQueryHandler(respond_with_audio)]
        },
        fallbacks= [CommandHandler("end", end)]
    )

    application.add_handler(conv_handler)
    # Run the bot until the user presses Ctrl-C
    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__=="__main__":
    main()

Step 3: Create utility functions

As you have probably seen from our workflow, our chatbot will require function(s) to take care of;

  • searching the news with a search engine - search

  • fetching our news content - webBaseLoader

  • summarizing it - generate_summary

  • generating our radio report - generate_radio_report

To achieve this, we will create a file named utils.py

# utils.py

from langchain_community.tools import DuckDuckGoSearchResults
from langchain_community.document_loaders import WebBaseLoader
from langchain.chat_models import init_chat_model
from spitch import Spitch
import spitch
import os
from dotenv import load_dotenv

load_dotenv()

summary_prompt = """You are a news reporter, I will provide a couple of news and your job 
will be to create a short detailed summary report based on them \\n #News: \n{}"""

base_query="Latest Nigerian {} news in town"

# To know the compatible model providers, check https://python.langchain.com/docs/integrations/chat/
# In our case, we are using `langchain-google-genai`
llm = init_chat_model("google_genai:gemini-2.5-flash", temperature=0.7)

def search(category):
    # Create the main query by merging the category into the base query
    query = base_query.format(category)

    search = DuckDuckGoSearchResults(
                num_results=5, 
                output_format="list", 
                backend="news"
            )
    search_results = search.invoke(query)
    # Return a list of results from the search engine
    return search_results


def retrieve(links):
    docs = []

    # Loop through each provided link and retrieve content from 
    # those that can be accessed.
    for link in links:
        try:
            loader = WebBaseLoader(link)
            doc=loader.load()
            docs.extend(doc)
        except Exception as e:
            print(f"Unable to get source from {link}", e)
    return docs


async def generate_summary(docs):
    # Create the main prompt, by merging the summary prompt
    # and the provided content.
    prompt = summary_prompt.format(docs)

    # Generate the summary through the predefined llm
    try:
        result= await llm.ainvoke(prompt)
        summary = result.content.strip("*")
    except Exception as e:
        print("Error, summarizing text", e)
        summary = "Sorry unable to generate summary"
    return summary

Now it is time to explore the Text-to-speech(TTS) offerings of Spitch.

  • Spitch provides TTS models in 5 different languages, we'll only be using 4 of them; English, Yoruba, Igbo and Hausa.

  • We will start by creating a TTS client(make sure you have gotten your api key for this section).

  • After configuring the client, we can generate an audio by specifying; the language, the voice(check here for more options), and the text we wish to convert.

# utils.py

# ...existing imports
from spitch import Spitch
import spitch
import os
from dotenv import load_dotenv

# ...existing code

translation_prompt = """You are an expert translator, given the below text you are 
to convert it to {} language. \\n #Text: \n{}"""
tts_client = Spitch(api_key=os.getenv('SPITCH_API_KEY'))

def translate(lang, text):
    # Create the main prompt, by merging the translation prompt
    # and the provided text.
    prompt = translation_prompt.format(lang, text)
    result = llm.invoke(prompt)
    return result.content


def generate_radio_report(lang, summary, voice):
    # Determine language to generate audio in.
    if lang != "en":
        text = translate(lang, summary)
    else:
        text = summary

    try:
        response = tts_client.speech.generate(
            language=lang,
            text=text,
            voice=voice,
        )
        return response.parse()
    except spitch.APIConnectionError as e:
        print(e.__cause__)  
    except spitch.APIStatusError as e:
        print(e.status_code)
    except Exception as e:
        print(e)

Step 4: Complete the workflow

Now that we have created our utilities, we can now complete our workflow. You will have to go back to the chatbot.py file to do this.

# chatbot.py

# ...existing imports
from utils import search, retrieve, generate_summary, generate_radio_report

async def start(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Begins the conversation flow, and prompts the user to 
    provide a news category.
    """

    # Retrieve the current message inputted by the user,
    # in this case "start".
    user = update.message.from_user
    logger.info("User's name is %s", user.first_name)

    # Create a keyboard object containing news category options.
    keyboard = [
        [InlineKeyboardButton("sports", callback_data='sports')],
        [InlineKeyboardButton("government", callback_data='government')],
        [InlineKeyboardButton("security", callback_data='security')],
        [InlineKeyboardButton("finance", callback_data='finance')],
        [InlineKeyboardButton("entertainment", callback_data='entertainment')],
        [InlineKeyboardButton("general", callback_data='general')],
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)

    # Render news category options along with a message to the user.
    await update.message.reply_text(
        "Welcome to the news bot, search for the latest news, from any category to the latest\n"
        'Choose an option:', reply_markup=reply_markup
    )

    return CATEGORY_HANDLER

In the above, we initiated our chat flow by sending a message to the user, using update.message.reply_text , additionally, we prompt the user to choose an option from the list provided in the keyboard variable.

# chatbot.py
# ...existing code

async def handle_category_callback(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Retreives news articles based on user's chosen category.

    Generates summary of the retrieved news, and prompts the user 
    to choose between a radio report or not.
    """ 

    # Retrieve user's selected category
    query = update.callback_query
    await query.answer() 
    category = query.data
    logger.info("User chose %s", category) 

    # Return a status message to the user
    status_message = await query.message.reply_text(f"Searching for {category} news...")

    # --- Search ---
    logger.info("Starting search for '%s' news.", category)
    search_results = search(category) 
    await status_message.edit_text(f"Found search results for {category}. Retrieving sources...")

    # --- Retrieve ---
    logger.info("Retrieving documents for '%s'.", category)

    # Unpack the results list and retrieve the link for each of them,
    # where each result is a dictionary with a "link" key.
    news_links = [result["link"] for result in search_results]
    retrieved_sources = retrieve(news_links) 
    await status_message.edit_text(f"Documents retrieved. Generating summary for {category}...")

    # --- Generate Summary ---
    logger.info("Generating summary for '%s'.", category)

    # Unpack the retreived text from each of the news sources and merge them
    joined_sources = "".join([source.page_content for source in retrieved_sources])
    # Remove any excess whitespace in the joined text
    news = " ".join(joined_sources.split())

    query_response = await generate_summary(news)
    news_summary = query_response

    # Store summary and category data for future reference.
    context.user_data['current_news_summary'] = news_summary
    context.user_data['selected_category'] = category

    # Create a keyboard object containing binary options
    keyboard = [
        [InlineKeyboardButton("yes", callback_data="yes")], 
        [InlineKeyboardButton("no", callback_data="no")], 
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)

    # Instead of directly replying now, the previous message is edited
    # and replaced with the generated summary.
    await status_message.edit_text(
        f"Here is a summary of the recent {category} news.\n"
        f"{news_summary}\\n"
        "Hope you liked it, would you like to get a radio report of this?",
        reply_markup=reply_markup
    )
    return FOLLOWUP_HANDLER

After the user selects a news category, the functions responsible for search, retrieval and summary are called from utils.py. Once the news content is fetched and summarized, it is then displayed to the user through status_message.edit_text. The user then get’s to decide if they want a radio report or not.

# chatbot.py
#...existing code

async def handle_follow_up(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Based on the user's decision, dynamically chooses whether 
    to end the chat interaction or follow up on the radio report.
    """
    # Retrieve user's selected option
    query = update.callback_query
    await query.answer()

    choice = query.data

    # If the user chooses "yes", create keyboard object containing
    # language options for the report, and render it along with a message.
    if choice == "yes": 
        keyboard = [
        [InlineKeyboardButton("English", callback_data="en")], 
        [InlineKeyboardButton("Yoruba", callback_data="yo")],
        [InlineKeyboardButton("Igbo", callback_data="ig")], 
        [InlineKeyboardButton("Hausa", callback_data="ha")], 
    ]

        reply_markup = InlineKeyboardMarkup(keyboard)

        await query.message.reply_text(
            "What language would you like this report in?.\n",
            reply_markup=reply_markup
        )
        return REPORT_HANDLER

    # If the user chooses "no", render an exit message and end
    # conversation.
    elif choice == "no":
        await query.message.reply_text("Okay, have a good day my friend.")
        return ConversationHandler.END

    # If the user chooses an invalid answer, render an exit message
    # and end conversation.
    else:
        await query.edit_message_text("Invalid choice. Conversation ended.")
        return ConversationHandler.END

If the user wishes to get a radio/audio report, they are prompted to choose a language; otherwise, the conversation ends.

# chatbot.py
#...existing code

async def respond_with_audio(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Returns an audio file based on the user's selected language.
    """
    # Retrieve selected radio report language
    query = update.callback_query
    await query.answer()

    language = query.data
    logger.info("User chose %s language", language)

    await query.edit_message_text("Preparing radio report, this might take a while...")

    # Retrieve earlier news summary 
    news_summary = context.user_data.get('current_news_summary')

    # More voice options can be found at https://docs.spitch.app/concepts/voices
    reporter = {"en":"lucy", "yo":"sade", "ig":"ebuka", "ha":"zainab"}.get(language)
    audio_obj = generate_radio_report(language, news_summary, reporter)

    # Return the radio report to the user
    await query.message.reply_audio(
        audio=audio_obj,
        performer=reporter,
        title="Radio Report"
    )

    # Render exit message then end.
    await query.edit_message_text("Radio report sent successfully! Enjoy.")
    context.user_data.clear()
    return ConversationHandler.END

Based on the user’s chosen language and news summary, the audio report is generated through the generate_radio_report function, and rendered through query.message.reply_audio. After which the conversation ends instantly.

# chatbot.py
#...existing code
async def end(update:Update, context:ContextTypes.DEFAULT_TYPE):
    """
    Ends conversation gracefully in the case of a user quitting.
    """
    # Render fallback exit message at the end conversation.
    await update.message.reply_text("Have a good day my friend")
    return ConversationHandler.END

Optionally, if a user intends to quit prematurely, the above end function will handle it.

# chatbot.py
#...existing code

def main():
    """
    Builds the conversation flow based on;
    - a bot token from @botfather
    - the defined states
    - the state handlers
    """
    application = Application.builder().token(os.getenv('TELEBOT_TOKEN')).build()

    # Create conversation handler
    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states= {
            CATEGORY_HANDLER : [CallbackQueryHandler(handle_category_callback)],
            FOLLOWUP_HANDLER : [CallbackQueryHandler(handle_follow_up)],
            REPORT_HANDLER : [CallbackQueryHandler(respond_with_audio)]
        },
        fallbacks= [CommandHandler("end", end)]
    )

    application.add_handler(conv_handler)
    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__=="__main__":
    main()

In our code above, you have seen that we have completed our chat flow to match our needs. Now it's time to run it and see the results for ourselves.

Step 5: Run the bot

To run the bot server, follow the below instructions:

  • In your telegram app(desktop, web or mobile), navigate to the telegram bot you created

  • Then in your terminal(IDE terminal or command line), navigate to your project directory and run the below

      python chatbot.py
    
  • Now in your bot channel, click "start”, this will begin our conversation workflow, which will render the options defined in the start function.

  • You can then generally continue the process, while observing the logs to see if your requests are getting through.

  • By the end you should have a news summary along with a radio report in the language of your choice.

Conclusion

Congratulations! You've successfully built a news radio bot using Spitch, Telegram, and Langchain. Feel free to experiment with different languages and news sources to customize your bot further.

0
Subscribe to my newsletter

Read articles from Simeon Adebola directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Simeon Adebola
Simeon Adebola

I am passionate about making impactful products