pythonを使って時報とニュースを自動読み上げ

昨日、兄が実家に帰ってきて、色々と面白い技術を教わりました。

1つがVoiceTextと呼ばれる、テキストから音声を作成してくれるWebサービス

もう1つがBeautifulSoupと呼ばれるhtml解析をしてくれるライブラリです。

この2つを組み合わせることで、webサイトから拾ってきたテキストを自動で読み上げることが出来ます。

昨日は兄と一緒に、ウェザーニュースのWebサイトから天気予報を拾ってきて、ラズパイで再生させるところまで作りました。

今日は一人黙々と改良して、地元鹿児島のMBCニュースとNHKのニュースを読み上げるところまで作りました。

また、時報機能も付けました。0800.txtというファイルを作ってcommandフォルダに置いておくと8時になったときにテキストを読み上げてくれるというものです。

“おはようございます。[HOUR]になりました。今日の天気です。[WEATHER]。今日のニュースです。[MBCNEWS]”

のようにコマンドも差し込めるようにしました。

なかなか良い出来になったので公開。

実行にはPythonがインストールされている必要があります

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import random
import datetime
import time
import locale

# pipでのインストールが必要
import requests                     # pip install requests
import pygame                       # pip install pygame
from bs4 import BeautifulSoup       # pip install BeautifulSoup4

# windowsのShift-JIS変換エラー防止(ラズパイではコメントアウトする)
locale.setlocale(locale.LC_CTYPE, "Japanese_Japan.932")

# VoiceTextのキー
voice_text_key = '****************'
voice_text_name = ["show", "haruka", "hikari", "takeru", "santa", "bear"]


# カレントディレクトリを同フォルダに設定
os.chdir(os.path.dirname(os.path.abspath(__file__))) 

# --- 関数定義 ---
# Webサイトのテキストを拾ってくる(URL, HTMLタグ, クラス名)
def GetWebTextByClass(url, tag, class_name):
    res = requests.get(url)
    soup = BeautifulSoup(res.text, "html.parser")
    item = soup.find(tag, class_=class_name)
    if item is None:
        return ""
    else:
        return item.get_text()

# Webサイトのテキストを拾ってくる(URL, HTMLタグ, ID)
def GetWebTextById(url, tag, id):
    res = requests.get(url)
    soup = BeautifulSoup(res.text, "html.parser")
    item = soup.find(tag, id=id)
    if item is None:
        return ""
    else:
        return item.get_text()

# Webサイトのテキストを拾ってくる(URL, HTMLタグ, name)
def GetWebTextByName(url, tag, name):
    res = requests.get(url)
    soup = BeautifulSoup(res.text, "html.parser")
    item = soup.find(tag, attrs={'name' : name})
    if item is None:
        return ""
    else:
        return item.get_text()

# ファイルを1行ずつ読み取りリストに変換
def ReadFileToLine(filename):
    try:
        f = open(filename, "r", encoding="utf-8_sig")
        lst =  list(f)
        f.close()
        cnt = len(lst)
        for i in range(cnt):
            lst[i] = lst[i].replace('\n', '')
        return lst
    except:
        return list()
    
# テキストを読み上げる
def VoiceTextReading(text, name_id = 0):
    global voice_text_key, voice_text_name
    #emotion = ["happiness", "anger", "sadness"]
    #emotion_level = [1, 2, 3, 4]    
    if len(text) >= 200:
        text = text[:200]

    data = {
        'text': text,
        'speaker': voice_text_name[name_id % len(voice_text_name)],
        'format' : 'mp3'
    }
    #'emotion' : random.choice(emotion),
    #'emotion_level' : random.choice(emotion_level)
    # 'pitch' : random.choice(range(150))+50,

    response = requests.post('https://api.voicetext.jp/v1/tts', data=data, auth=(voice_text_key, ''))
    try:
        with open('./temp.mp3', 'wb') as f:
            f.write(response.content)
            f.close()
            
        pygame.mixer.init()
        pygame.mixer.music.load('./temp.mp3')
        #s = pygame.mixer.Sound(response.content)
        pygame.mixer.music.play()        
        while pygame.mixer.music.get_busy() == True:
            time.sleep(1)
            continue
        pygame.mixer.quit()
        time.sleep(1)
    except:
        time.sleep(1)
        return

# 200文字に収まるように句点で切り分けてからテキストを読み上げる
def LongVoiceTextReading(text):
    # 。で切り分ける
    list_text = text.split("。")

    # 200文字に収まるように出力
    output  = ""
    for t in list_text:
        if len(output) + len(t) <= 196:
            output += "。"
            output += t
            continue
        VoiceTextReading(output)
        output = t



    if output != "":
        VoiceTextReading(output)


# MBCニュースサイトから記事を取得 [[時間datetime, "タイトル", "記事内容"]] の2重リストで返す
def GetMBCNews(after=None):
    ret = list()
    res = requests.get("https://www.mbc.co.jp/news/")
    text = res.text.encode("raw_unicode_escape").decode("utf8")
    soup = BeautifulSoup(text, "html.parser")
    ct = soup.find("div", id="mbcnews-top").contents

    # 日付
    now = datetime.datetime.now()
    for c in ct:
        if c == '\n':
            continue
        soup = c
        if soup.name == "h2":                       # h2タグから日付を取得
            day = soup.attrs["id"]                  # 210516
            year = int("20" + day[0:2])
            month = int(day[2:4])
            day = int(day[4:6])
            dt = now.replace(year=year, month=month, day=day)   # 日付データの年月日を更新
        elif soup.name == "li":                     # liタグから各ニュースを取得
            tm = soup.find("span").get_text()       # [02:10]
            hour = int(tm[1:3])
            min = int(tm[4:6])
            dt2 = dt.replace(hour=hour, minute=min) # 日付データの時間を更新
            if after is not None and after >= dt2:   # 指定日付より古い記事になったら終了
                break
            ret2 = list()
            ret2.append(dt2)
            texts = soup.get_text().split(tm)       # 同時取得されたタイトルと本文を[XX:XX]で区切る
            ret2.append(texts[0])  
            ret2.append(texts[1])
            ret.append(ret2)
    return ret

def GetNHKNews(after=None):
    ret = list()
    html = requests.get("https://www3.nhk.or.jp/news/catnew.html")
    html_dec = html.text.encode("raw_unicode_escape").decode("utf8").replace('\n', '')
    soup = BeautifulSoup(html_dec, "html.parser")
    #with open("news.txt", "w", encoding="utf8") as f:
    #    f.write(text)
    child_tag = soup.find("ul", class_="content--list").contents
    
    # 日付
    now = datetime.datetime.now()
    for tag in child_tag:
        if tag == '\n':
            continue
        soup2 = tag
        if soup2.name != "li":
            continue
        em = soup2.find("em").get_text()            # 見出し
        tm = soup2.find("time")                     # 時間
        tm_str = tm["datetime"]                     # "2021-05-16T15:12:45"
        a = soup2.find("a")

        # リンク先の記事を読み込む
        url = "https://www3.nhk.or.jp" + a["href"]
        html = requests.get(url)
        html_dec = html.text.encode("raw_unicode_escape").decode("utf8").replace('\n', '')
        soup = BeautifulSoup(html_dec, "html.parser")
        text = soup.find("p", class_="content--summary").get_text()
        more = soup.find("p", class_="content--summary-more")           # 更に読むの領域
        if more is not None:
            text += more.get_text()

        # 日付をdatetime型に変換
        dt = datetime.datetime.strptime(tm_str, '%Y-%m-%dT%H:%M:%S')
        if after is not None and after >= dt:        # 指定日付より古い記事になったら終了
            break
 
        l = list()
        l.append(dt)        # 日付
        l.append(em)        # 見出し
        l.append(text)      # 本文
        ret.append(l)       # 戻り値に追加
    return ret

def SaveNews(filename, news):
    # 使用例(MBCニュースをnews.txtに出力する)
    with open(filename, "w", encoding="utf8") as f:
        for n in news:
            # 日付
            text = n[0].strftime("%Y年%m月%d日 %H:%M")
            # タイトル
            text += " ["
            text += n[1]
            text += "]"
            # 本文
            text += n[2]
            # 改行
            text += "\n"
            f.write(text)

# --- ここよりメイン処理 ---
#today = datetime.datetime.now().replace(hour=0, minute=0)
#news = GetNHKNews(today)
#SaveNews("news.txt", news)

last_dt_mbc = datetime.datetime.now()    # MBCのチェック済み時間
last_dt_nhk = datetime.datetime.now()    # NHKのチェック済み時間

m_log = 0
while(1):
    # 1分おきに更新処理を行う
    dt = datetime.datetime.now()
    if m_log == dt.minute:
        time.sleep(1)
        continue
    m_log = dt.minute

    # commandファイルによる時報 & ニュース読み上げ
    filename = dt.strftime("./command/%H%M.txt")
    if os.path.exists(filename):
        l = ReadFileToLine(filename)
        for t in l:
            if t == "[MBCNEWS_TODAY]":                  # MBCニュースコマンド: MBCニュースの一覧を読み上げる(本日全て)
                news = GetMBCNews(datetime.datetime.now().replace(hour=0, minute=0))  # 今日のニュースを読み上げる
                last_dt_mbc = datetime.datetime.now()
                for n in reversed(news):                    #古い順に
                    VoiceTextReading(n[2])                      # ニュース本文を読み上げる。 n:[時間, タイトル, 本文]
            elif t == "[MBCNEWS]":                      # MBCニュースコマンド: MBCニュースの一覧を読み上げる
                news = GetMBCNews(last_dt_mbc)              # 最後に取得した時間以降のニュースを取得
                last_dt_mbc = datetime.datetime.now()
                if len(news) > 0:
                    VoiceTextReading("エムビーシー新着ニュースです")
                for n in reversed(news):                    # 古い順に
                    VoiceTextReading(n[2])                      # ニュース本文を読み上げる。 n:[時間, タイトル, 本文]
            elif t == "[NHKNEWS]":                      # NHKニュースコマンド: MBCニュースの一覧を読み上げる
                news = GetNHKNews(last_dt_nhk)              # 最後に取得した時間以降のNHKニュースを取得
                last_dt_nhk = datetime.datetime.now()
                if len(news) > 0:
                    VoiceTextReading("エヌエイチケー新着ニュースです")
                for n in reversed(news):
                    VoiceTextReading(n[1])              # ニュース本文を読み上げる。 n:[時間, タイトル, 本文]
            elif t == "[WEATHER]":                      # WEATHERコマンド: ウェザーニュースのコメントを読み上げる
                talk = GetWebTextByClass("https://weathernews.jp/onebox/tenki/kagoshima/46204/", "div", "comment no-ja")
                VoiceTextReading(talk)
            else:
                t = t.replace('[TODAY]', datetime.datetime.now().strftime("%m月%d日"))
                t = t.replace('[HOUR]', datetime.datetime.now().strftime("%H時"))
                t = t.replace('[MINUTE]', datetime.datetime.now().strftime("%M分"))
                VoiceTextReading(t)
 
    # NHKニュースは毎分チェック
    news = GetNHKNews(last_dt_nhk) 
    if len(news) > 0:
        VoiceTextReading("エヌエイチケー新着ニュースです")
    for n in reversed(news):
        LongVoiceTextReading(n[2])              # ニュース本文を読み上げる。 n:[時間, タイトル, 本文]
        last_dt_nhk = n[0]                      # 最終読み上げ時間を更新
    
    time.sleep(1)

voice_text_key のところはVoiceTextに無料登録をして送られてきたキーを置きます。

ウェザーニュースのURLは住んでいる地域に書き換えてください。

MBCは・・・鹿児島県民の気分になって聞いてくださいm(_ _)m

内容としては、1分おきにNHKニュースをチェックして、更新があればそれを読み上げます。

また、指定時間になったらcommandフォルダにあるテキストの内容を読み上げます。(ファイル名が時分になってます)

ダウンロードしたzipにサンプルが入っています。

感想

VoiceTextの音声合成技術が本当に凄くて、ニュースもアナウンサーが読み上げてるようなレベルです。
(相撲の記事は読み間違えが多くて笑えますが)

AIに声を与えることができるツールと出会えたのだとwktkしてます。

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です