# file name : reidai13_7.py
# reidai13_7 音声録音と再生、文字起こし

import streamlit as st
import wave,io                    # WAVをメモリ上で読み書きする標準ライブラリ
import pyaudio                    # マイクから「生の音」を取り出す
import numpy as np                # (5)で利用
import matplotlib.pyplot as plt   # (5)で利用
import matplotlib_fontja          # (5)で利用
import speech_recognition as sr   # (6)で利用

# ------ 初期設定------
# 音声データを16 ビットで設定 PCM（パルス符号変調）
FMT=pyaudio.paInt16  
# 'chunks' キーの値に空のリスト[] を代入
st.session_state.setdefault('chunks',[])
# 'wav_bytes' キーの値に空のバイト列b'' を代入
st.session_state.setdefault('wav_bytes',b'')

# ====== UI ======
st.set_page_config(page_title='音声ツールアプリ')
st.title('音声ツールアプリ')

# ====== 準備：録音条件の設定（サイドバー設定） ======
st.sidebar.header('録音条件設定')
# サンプリング周波数 [Hz]の選択…1分間に取集するデータ量、CD:44100Hz
# 引数：表示する文字列, 最小値, 最大値, 初期値, ボタン利用時の刻み幅
SRATE=st.sidebar.number_input('サンプリング周波数 [Hz]',
                               8000,48000,16000,step=1000)
# チャンネルの選択…1=モノラル、2=ステレオ(L,R）
CH=st.sidebar.selectbox('チャンネル数',[1,2],index=0)
# まとめて読み書きする処理単位の選択
chunk_lst=[512,1024,2048,4096]  
CHUNK=st.sidebar.selectbox('CHUNK',chunk_lst,index=1)
# 録音時間(秒) の選択…引数:1～30(秒)、初期値5(秒)
SEC =st.sidebar.slider('録音時間(秒)',1,30,5)

# ===== (1)マイクの検出と選択 =====
def list_input_devices():
    pa=pyaudio.PyAudio()                             # PyAudioの管理オブジェクト
    devices=[]
    for i in range(pa.get_device_count()):           # 全デバイスを調べる
        info=pa.get_device_info_by_index(i)          # 辞書型デバイス情報獲得
        if info.get('maxInputChannels',0)>0:         # 使用可能なものだけ
            label=f'{i}:{info['name']}(in={info['maxInputChannels']}, \
            rate={int(info['defaultSampleRate'])})'
            devices.append((label,i))                # 表示名と内部番号のセット
    pa.terminate()                                   # 使い終わったら終了
    return devices
# 入力デバイス(マイク)リストからマイクの選択
dev_lst=list_input_devices()
st.header('(1)マイクの検出と選択')
if not dev_lst:
    st.error('入力デバイス（マイク）が見つかりません。')
    st.stop()
labels,ids=zip(*dev_lst) # 使用するマイクとidの抽出
# ------マイク選択 ------
dlabel_lst=st.selectbox('使用するマイク',labels,index=0)
INPUT_IDX=ids[labels.index(dlabel_lst)]              # 使用するマイクのインデックス
st.success(f'マイクOK（{dlabel_lst}）')

# ===== (2) 音声の録音=====
st.header('(2) 音声の録音')
if st.button(f'{SEC}秒録音する',use_container_width=True):
    st.session_state['chunks']=[]                    # 前回の録音結果をクリア
    pa=pyaudio.PyAudio()
    stream=None
    try:
        # マイク用「通路」を開く（録音なのでinput=True）
        stream=pa.open(format=FMT,channels=CH,rate=SRATE,
                       input=True,input_device_index=INPUT_IDX,
                       frames_per_buffer=CHUNK)
        st.info('録音中…')
        # 総フレーム回数=(1 秒あたりの読み取り回数)*(秒数)
        n_reads=int(SRATE/CHUNK*SEC)
        for _ in range(n_reads):
            # オーバーフロー時もエラーにせず録音を継続
            cnk_bytes=stream.read(CHUNK,exception_on_overflow=False)
            st.session_state['chunks'].append(cnk_bytes)
        st.success('録音完了')
    except Exception as e:
        st.error(f'録音失敗:{e}')
    finally:
        stream.stop_stream();stream.close()          # ストリームを閉じる
        pa.terminate()
st.caption(f" chunks のチャンク数:{len(st.session_state['chunks'])}")

# ===== (3) 音声をWAV ファイルに保存=====
def frames_to_wav_bytes(frames,rate,ch,fmt) :
    buf=io.BytesIO()
    pa=pyaudio.PyAudio()
    try:
        with wave.open(buf,'wb') as wf:              # メモリにWAV を書き込む
            wf.setnchannels(ch)                      # チャンネル(1=モノラル,2=ステレオ)
            wf.setsampwidth(pa.get_sample_size(fmt)) # サンプル幅
            wf.setframerate(rate)                    # サンプリング周波数
            wf.writeframes(b''.join(frames))         # 生PCM を連結し書き込む
            return buf.getvalue()                    # 完成したWAV のバイト列を返す
    finally:
        pa.terminate()                               # 終了
st.header('(3) 音声をWAV ファイルに保存')
# Web ページを横に2 分割し表示
col_left,col_right=st.columns(2)
with col_left:
    make=st.button('WAV データを作る',use_container_width=True,
        disabled=(len(st.session_state['chunks']) == 0))
    if make:
        st.session_state['wav_bytes']=frames_to_wav_bytes(
        st.session_state['chunks'],SRATE,CH,FMT )
        st.success('WAV データを作成しました。')
with col_right:
    if st.session_state['wav_bytes']:
        fname=st.text_input('保存ファイル名','recorded.wav')
        st.download_button('WAV を保存',
            st.session_state['wav_bytes'],
            file_name=fname,mime='audio/wav',
            use_container_width=True)
        
# ===== (4) 録音したWAV ファイルの再生=====
st.header('(4) 録音したWAV ファイルの再生')
if st.session_state['wav_bytes']:
    st.audio(st.session_state['wav_bytes'],format='audio/wav')
else:
    st.info('録音→ WAV 作成後に再生できます。')
# ===== (5) 録音した音声の波形の表示=====
st.header('(5) 録音した音声の波形の表示')
if st.session_state['wav_bytes']:
    with wave.open(io.BytesIO(st.session_state['wav_bytes'])
    ,'rb') as wf:
        n_ch=wf.getnchannels()                        # 1=モノラル,2=ステレオ
        sw =wf.getsampwidth()                         # 2 なら16bit
        fr =wf.getframerate()                         # サンプルレート
        n_fr=wf.getnframes()                          # 総フレーム数
        raw =wf.readframes(n_fr)                      # 生サンプルをすべて読み込む
    # サンプル幅からNumPy のdtype を決定
    dtype=np.int16 if sw==2 else np.int8
    y=np.frombuffer(raw,dtype=dtype)
    if n_ch==2: y=y.reshape(-1,2)[:,0]                # 左ch を選択
    t=np.arange(len(y))/fr                            # 横軸(秒)(サンプル数/サンプルレート)
    # グラフ描画
    fig,ax=plt.subplots(figsize=(6,2.5))
    ax.plot(t,y)
    ax.set(xlabel='Time(s)',ylabel='Amplitude',title='Waveform')
    st.pyplot(fig)
else:
    st.info('WAV データを作成すると波形を表示します。')

# ===== (6) 音声ファイルの文字起こし=====
lang=st.sidebar.text_input('言語プログラム','ja-JP')   # サイドバーで設定
st.header('(6) 音声ファイルの文字起こし')
if st.button('文字起こしする',use_container_width=True,
    disabled=(not st.session_state['wav_bytes'])):
    r=sr.Recognizer()
    # メモリ上のWAV をAudioFile に渡す（ファイル保存は不要）
    with sr.AudioFile(io.BytesIO(st.session_state['wav_bytes'])) \
            as source:
        audio=r.record(source)                        # 全区間
    try:
    # 認識（オンラインのGoogle Web Speech API を使用
        text=r.recognize_google(audio,language=lang)
        st.success('認識結果'); st.write(text)
    except sr.UnknownValueError:
        st.error('音声を理解できませんでした。')
    except sr.RequestError as e:
        st.error(f'サービスに接続できません:{e}')