19日に更新してた

アフィリエイトはないよ

【python】旺文社 Target1900 のダウンロードコンテンツを分割してみた

旺文社 Target1900 のダウンロードコンテンツの音声は数単語ごとにまとまっているが、1単語ごとの音声ファイルが欲しい

Abceed で旺文社 Target1900 の単語単位の音声を復習が必要なものだけ聞けるようにしたかったが、機能がないのかどうもはかばかしくない*1。しかたがないので音声ダウンロードサービスを利用してみる。しかし、ダウンロードコンテンツの音声が数単語ごとにまとまっているので、全部をプレイリストに入れておいてわかるものから消していくという方法が使えない。

しかたがないので、分割しようと思います。

とりあえず、3.見出し語の意味→見出し語 を一括ダウンロードして win11 のデスクトップにおいてある設定です。

流行りに乗って AI を使ってみたいので Whisper を試してみる

最終的に ffmpeg を使うとして分割時間設定をどうするかと、「流行りに乗って AI や!」と Whisper を google colab で試してみる。
2言語混在しているとあまり精度が良くないとのことも、認識言語を日本語にするとそこそこ認識してくれるが、2単語分を一文扱いにしてくれたりする。認識言語を英語とかスペイン語とかにして試してみるも使えそうになかったので30分ぐらいいじって撤退。*2

Audacity で波形を見て、ffmpeg で全面的にやることに

www.audacityteam.org

波形見るのに Audacity を使ってダウンロードしたファイルを見てみると、単語ファイル間は1秒ほど空いている模様。ffmpeg で無音感知があると便利だなぁと検索してみると、silencedetect というオプションがありました。なので全面的にffmpeg 系でいくことに決定。

自動で全部やるのは自分の技術的に無理そうなので、ffplay で聞いて選り分ける

下に書いたコード以外にも silencedetect で stderr に出てきたのを正規表現で抜き出して、無音の最初と最後をリストにして zip 関数使って組み合わせて数数えてとかやっているのですが、大体下のに入っていたので割愛。

jupyter notebook 上で

import subprocess
import re
import os
from IPython.display import clear_output
import pickle

dirs=os.path.join(os.path.expanduser("~"),"Desktop","1900_3_all")
cwd=os.getcwd()
os.chdir(dirs)
s={}
dirlist=sorted([f for f in os.listdir(dirs) if f.endswith(".mp3")])

# cc=76

for n,f in enumerate(dirlist):
    
#     if n<cc:
#         continue
#     if n==cc:
#         s=pickle.load(open("./test.pkl","rb"))
    
    file=os.path.join(dirs,f)
    a=subprocess.run(f"ffmpeg -i {file} -af silencedetect=n=0.001:d=1:m=0 -vn -f null -", capture_output=True, text=True,encoding="utf-8")

    file_end=[float(f) for f in re.findall('silence_start: (\d+.\d+)\n',a.stderr)]
    file_start=[0]+[float(f) for f in re.findall('silence_end: (\d+.\d+)',a.stderr)]
    
    a,b,c=os.path.splitext(f)[0].rsplit("_",2)
        
    idealfiles = 1+int(c)-int(b)
    
    file_time=[(s,t) for s,t in zip(file_start,file_end)]
    if idealfiles==len(file_time):
        continue 
        
    checked=0

    for i,t in enumerate(file_time):
        print(f"{n} {i+1}/{checked}/{1+int(c)-int(b)}/{len(file_time)},{n}/{len(dirlist)}")
        if checked+(1+int(c)-int(b))==len(file_time):
            continue

        subprocess.run(f"ffplay -i {f} -ss {t[0]} -t {t[1]-t[0]+0.5}")
        aa=input("OKならEnter、問題ありなら文字入力後Enter")
        clear_output()
        if aa:
            s.setdefault(n,[]).append(i)
            checked+=1
    pickle.dump(s,open("./test.pkl","wb"))
    
    
os.chdir(cwd)
s

途中で休む対策に pickle 使って途中から始められるようにして*3、後から再度起動した場合に dirlist が pickle.dump 前と変わらないように内包表記使って endswith でなんとかしてます。
ファイルのはじめから音声部分まで1秒無音状態のファイルはなさそうなので file_start のリストの最初の0足して合わせてます。

表示をクリアしてわかりやすくしたり、ファイル名からそのファイル内の単語数を計算して、分割後のファイル数との差をチェックしたら後は飛ばすようにして、手間を惜しんでます。*4

そうして作った削除するファイルの Dictionary がこちら。

s={0: [0, 1, 2],
 2: [11],
 5: [2, 24],
 6: [0, 1],
 8: [12],
 11: [2, 24],
 12: [0, 1],
 14: [10],
 17: [2, 24],
 18: [0, 1],
 20: [10],
 23: [1, 22],
 24: [0, 1],
 26: [10],
 29: [2, 24],
 30: [0, 1],
 32: [11],
 34: [17],
 36: [0, 1],
 38: [11],
 41: [3, 25],
 42: [0, 1],
 44: [10],
 47: [0, 22],
 48: [0, 1, 2],
 50: [6],
 52: [10],
 53: [14],
 54: [0, 1],
 56: [7],
 58: [11],
 59: [15],
 60: [0, 1],
 62: [7],
 64: [11],
 65: [15],
 66: [0, 1],
 68: [8],
 70: [11],
 72: [0, 1],
 74: [9],
 76: [13],
 78: [0, 1],
 80: [9],
 82: [12],
 84: [0, 1],
 86: [8],
 88: [12],
 90: [0, 1, 2],
 92: [5],
 94: [5],
 95: [0, 1],
 97: [5],
 99: [5],
 100: [0, 1],
 102: [4],
 104: [5],
 105: [0, 1],
 107: [5],
 109: [5]}

なんで s なのかは今の僕にもわかりません。その場ののりです。
1989ファイルに分割されていたものを89減らすので数は合いました。

ここまでは前置きなので、次行きましょう。*5

外見だけは公式っぽく行きたいのでカバーアートとかタグとか

カバーアートは手持ちのファイルから抜いておきます。

#1900_3_allディレクトリ内で
ffmpeg -i TG1900_3_Sec01_0001_0014.mp3 cover.png

で、ffprobe も使ってこんなふうに。

s={0: [0, 1, 2],
 2: [11],
 5: [2, 24],
 6: [0, 1],
 8: [12],
 11: [2, 24],
 12: [0, 1],
 14: [10],
 17: [2, 24],
 18: [0, 1],
 20: [10],
 23: [1, 22],
 24: [0, 1],
 26: [10],
 29: [2, 24],
 30: [0, 1],
 32: [11],
 34: [17],
 36: [0, 1],
 38: [11],
 41: [3, 25],
 42: [0, 1],
 44: [10],
 47: [0, 22],
 48: [0, 1, 2],
 50: [6],
 52: [10],
 53: [14],
 54: [0, 1],
 56: [7],
 58: [11],
 59: [15],
 60: [0, 1],
 62: [7],
 64: [11],
 65: [15],
 66: [0, 1],
 68: [8],
 70: [11],
 72: [0, 1],
 74: [9],
 76: [13],
 78: [0, 1],
 80: [9],
 82: [12],
 84: [0, 1],
 86: [8],
 88: [12],
 90: [0, 1, 2],
 92: [5],
 94: [5],
 95: [0, 1],
 97: [5],
 99: [5],
 100: [0, 1],
 102: [4],
 104: [5],
 105: [0, 1],
 107: [5],
 109: [5]}

import subprocess
import re
import os
import time

dirs=os.path.join(os.path.expanduser("~"),"Desktop","1900_3_all")
pic=os.path.join(dirs,"cover.png")
out_dir_name="Target1900JE"
out_dir=os.path.join(dirs,out_dir_name)
cwd=os.getcwd()
os.chdir(dirs)
if not os.path.isdir(out_dir):
    os.mkdir(out_dir)
dirlist=sorted([f for f in os.listdir(dirs) if f.endswith(".mp3")])
j=1
for n,f in enumerate(dirlist):

    file=os.path.join(dirs,f)
    a=subprocess.run(f"ffmpeg -i {file} -af silencedetect=n=0.001:d=1:m=0 -vn -f null -", capture_output=True, text=True,encoding="utf-8")

    file_end=[float(f) for f in re.findall('silence_start: (\d+.\d+)\n',a.stderr)]
    file_start=[0]+[float(f) for f in re.findall('silence_end: (\d+.\d+)',a.stderr)]
    
    a,b,c=os.path.splitext(f)[0].rsplit("_",2)
        
    idealfiles = 1+int(c)-int(b)
    
    file_time=[(s,t) for s,t in zip(file_start,file_end)]
    
    gg=subprocess.run(f"ffprobe -i {file}", capture_output=True, text=True,encoding="utf-8")
    title0=re.findall("(Section \d+\D\d+\D+)", gg.stderr)[0]

    for i,t in enumerate(file_time):

        if n in s.keys():
            if i in s.get(n) :
                continue
        nums="_".join([a,str(j).zfill(4)])
        out_put_file=os.path.join(out_dir,nums+".mp3")
        
        title=title0+str(j)
        command=f'ffmpeg.exe -ss {t[0]} -t {t[1]-t[0]+0.5} -i {file} -i {pic} -map 0:a -map 1:v -c copy -disposition:1 attached_pic -id3v2_version 3 -map_metadata 0 -metadata title="{title}" -metadata track={j}  {out_put_file}'

        subprocess.run(command)
        j+=1
        time.sleep(0.1)
os.chdir(cwd)
print("終了")

後は勉強するだけ

おまけ

コチラで Target1900 の語彙リストを上げていただいています。

ukaru-eigo.com

ここの表をお借りしてきて、こんな感じで

import pandas as pd

url='https://ukaru-eigo.com/target-1900-word-list/'
df = pd.read_html(url)
words=df[0]["単語"].tolist()
jwords=[w.split(",")[0].split(";")[0].split("(⇔")[0].split("(≒")[0] for w in df[0]["意味"].tolist()]

tagtitle=[w0+" "+w1 for w0,w1 in zip(jwords,words)]
tagtitle

>>['を創り出す create',
>> '増加する increase',
>> 'を向上させる improve',
>> 以下略

ffmpeg の metadata の {title} を {tagtitle[j-1]} とでもしてやるとプレイヤータイトル表示が 'を創り出す create' などとなります。

copilot で作った"【python】旺文社ターゲット1900のダウンロードコンテンツを分割してみた。"の画像

*1:元々、本を買ったけれど覚えている、いないの管理が面倒なのでアプリを買おうと思い、セールに目を引かれてこちらを購入。公式だと1冊分だけだけれど買い切りで値段が安いみたい。そちらだとこの機能、あるのかも

*2:ここらへんは検索したら出てくるそのままなのでコードは省略。

*3:コメントアウトしている cc に再開したい n を入れる

*4:全部聞くつもりだったのですが、画面は流れる、数は多いで、途中で何やっているのかわからなくなって来たので追加した

*5: 上記の設定で調べるとダウンロードコンテンツの1,2,3は1989、4は1991で、全部聞いたわけではないのですが、減らす方のファイルは1,2,3ともこの Dictionary の中身で抽出できていたので、できているはず。