파이썬 입코딩, TTS 1차, 무료 TTS 발견, MP3 vs WAV

소개

영어, 중국어, 한국어 WAV TTS, 네이버 클라우드가 유료 서비스였는데

  • 온라인 영어, 중국어, 한국어 학원을 운영중, 영상 교재를 만들때 음성 변환이 필요

  • 기존에는 파이썬 입코딩, 구글의 gTTS 라이브러를 통해 mp3 파일을 제작해서 사용

  • mp3 파일이 몸에 좋지 않은 영향을 준다는 질문을 받고, 검색을 해본결과

  • https://blog.naver.com/osam_english/222045696033

  • 네이버, 아마존, 구글 등 클라우드 TTS는 WAV 파일을 지원

    • 그래서 유로인 네이버 클라우드 서비스를 사용하여 wav로 교재를 다시 제작

    • 네이버는 유료서비스로 api를 이용해야함

  • Cursor 입코딩을 하다가 pyttsx3 라는 파이썬 무료 라이브러리를 발견

진행 방법

어떤 도구를 사용했고, 어떻게 활용하셨나요?

Tip: 사용한 프롬프트 전문을 꼭 포함하고, 내용을 짧게 소개해 주세요.

Tip: 활용 이미지나 캡처 화면을 꼭 남겨주세요.

Tip: 코드 전문은 코드블록에 감싸서 작성해주세요. ( / 을 눌러 '코드 블록'을 선택)

(내용 입력)

  1. 커서를 이용 wav 제작용 파이썬 라이브러리 발견하고

  2. 커서 입코딩을 통해 파이썬으로

    1. 기존의 gTTS mp3 영어 단어장 대신

    2. pyttsx3 wav 영어 단어장을 제작

  3. 처음에는 커서가 mp3를 wav로 변환할 것을 제안해서, 다시 질문후 차이점 다시 확인

    한국사이트 스크린샷
  4. 고급음질 음성을 만드는 법을 문의한 바

    1. pyttsx3 및 유료서비스인 아마존, 구글, IBM 등 유료 서비서를 소개

    2. 무료인 pyttsx3 라이브러리도 무손실 형식의 wav 파일을 제작 가능

      한국어 웹사이트 스크린샷

결과와 배운 점

배운 점과 나만의 꿀팁을 알려주세요.

과정 중에 어떤 시행착오를 겪었나요?

  • 처음에는 유료 서비스만 있는 줄 알고 한국어 발음이 좋을거로 예상 되는 네이버 클라우드 사용

  • 월 6만원 정도 사용료로 지불,

  • 오늘 무료 상품 발견으로 해지 예정

앞으로의 계획이 있다면 들려주세요.

  • 다른 어학자료도 wav 파일로 제작하여 보급

  • 중국어도 가능한지 확인

도움 받은 글 (옵션)

커서 에이전트 사용

13기 커서 스터디에서 배운 에이전트 기능을 사용하면서

파이썬 입코딩이 한단계 더 쉬워짐

https://www.gpters.org/ai-study-list/post/english-5cIBD62o4R794VB

동영상 영어 단어장 파이썬 코드(맥북)

import openpyxl
import tkinter as tk
from tkinter import messagebox
from tkinter import ttk
import os
import subprocess
from pathlib import Path
import time
import json

# 경로 설정
SCRIPT_DIR = Path(os.path.dirname(os.path.abspath(__file__)))

# 설정 파일 경로
SETTINGS_FILE = SCRIPT_DIR / 'settings.json'

# 엑셀 파일 경로
EXCEL_PATH = SCRIPT_DIR / 'words.xlsx'

# 기본 설정값
DEFAULT_SETTINGS = {
    'start_row': 1,          # 시작 행
    'end_row': 100,          # 종료 행
    'english_repeat': 1,     # 영어 반복 횟수
    'korean_repeat': 1,      # 한글 반복 횟수
    'word_delay': 0.3,       # 다음 단어까지 대기 시간
    'spacing': 0.3,          # 한영 간격
    'speed': 1.3,            # 음성 속도
    'direction': "영한",      # 학습 방향 (영한/한영)
    'ko_voice': 'Yuna',      # 한글 음성 (Premium)
    'en_voice': 'Karen',      # 영어 음성 (남성 음성으로 변경)
    'en_voice_list': [        # 사용 가능한 영어 남성 음성 리스트
        'Alex',
        'Bruce',
        'Fred',
        'Richard',
        'Vicki',
        'Ralph',
        'Thomas',
        'Arthur',
        'Daniel',
        'John',
        'Karen',        
        'Samantha'        
    ],
    'ko_voice_list': [        # 사용 가능한 한글 음성 리스트
        'Yuna',
        'Ji-eun'
    ]
}

def load_settings():
    try:
        if SETTINGS_FILE.exists():
            with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
    except Exception as e:
        print(f"설정 파일 로드 오류: {e}")
    return DEFAULT_SETTINGS

def save_settings(settings):
    try:
        with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
            json.dump(settings, f, ensure_ascii=False, indent=4)
    except Exception as e:
        print(f"설정 파일 저장 오류: {e}")

def get_words_from_excel(start_row, end_row):
    try:
        workbook = openpyxl.load_workbook(EXCEL_PATH)
    except FileNotFoundError:
        messagebox.showerror("에러", f"words.xlsx 파일을 찾을 수 없습니다.\n다음 위치에 파일이 있는지 확인해주세요:\n{EXCEL_PATH}")
        return []
        
    sheet = workbook.worksheets[0]
    words = []
    for row in range(start_row + 1, end_row + 2):
        english_word = sheet[f'A{row}'].value
        korean_meaning = sheet[f'B{row}'].value
        if english_word and korean_meaning:  # None 값 체크
            words.append((english_word, korean_meaning))
    return words

def speak(text, filename='output.wav', rate=150, is_korean=False, ko_voice='Shelley', en_voice='Alex'):
    # macOS의 say 명령어를 사용하여 직접 텍스트를 음성으로 변환
    speed = int(175 * (rate/300))  # 적절한 속도로 조정
    
    # 음성 변수 확인
    print(f"[DEBUG] Speaking '{text}' - Korean: {is_korean}, Voice: {'Ko Voice: ' + ko_voice if is_korean else 'En Voice: ' + en_voice}")
    
    # 음성 선택 및 실행
    try:
        if is_korean:
            # 한글인 경우
            cmd = ['say', '-v', ko_voice, '-r', str(speed), 
                  '--quality=128', '--data-format=LEF32@32000',  # 고품질 설정
                  text]
            print(f"한글 발음: {text} (음성: {ko_voice})")  # 디버깅용
        else:
            # 영어인 경우
            cmd = ['say', '-v', en_voice, '-r', str(speed),
                  '--quality=128', '--data-format=LEF32@32000',  # 고품질 설정
                  text]
            print(f"영어 발음: {text}")  # 디버깅용
        
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        print(f"음성 발음 오류: {e}")  # 오류 발생시 출력

def show_and_read_words(start_row, end_row, english_repeat, korean_repeat, speed, word_delay, spacing, is_eng_to_kor, en_voice, ko_voice):
    print(f"[DEBUG] show_and_read_words called with English Voice: {en_voice} and Korean Voice: {ko_voice}")
    words = get_words_from_excel(start_row, end_row)
    total_words = len(words)

    root = tk.Tk()
    root.title("단어 학습")
    root.geometry("800x700+0+0")
    root.configure(bg='#2e2e2e')

    is_paused = False
    current_after_id = None

    number_frame = tk.Frame(root, bg='#2e2e2e')
    number_frame.place(relx=0.5, rely=0.1, anchor='center')

    spacer_left = tk.Label(number_frame, text="", width=10, bg='#2e2e2e')
    spacer_left.pack(side='left')

    label_index = tk.Label(number_frame, text="", font=("Helvetica", 80, "bold"), fg='white', bg='#2e2e2e')
    label_index.pack(side='left')

    label_progress = tk.Label(number_frame, text="", font=("Helvetica", 24), fg='white', bg='#2e2e2e')
    label_progress.pack(side='left', pady=30)

    label_first = tk.Label(root, text="", font=("Helvetica", 80, "bold"), fg='white', bg='#2e2e2e')
    label_first.place(relx=0.5, rely=0.35, anchor='center')

    label_second = tk.Label(root, text="", font=("Helvetica", 80, "bold"), fg='white', bg='#2e2e2e')
    label_second.place(relx=0.5, rely=0.55, anchor='center')

    label_next_word = tk.Label(root, text="", font=("Helvetica", 40), fg='white', bg='#2e2e2e')
    label_next_word.place(relx=0.5, rely=0.7, anchor='center')

    info_frame = tk.Frame(root, bg='#2e2e2e')
    info_frame.place(relx=0.5, rely=0.85, anchor='center')

    spacing_info = tk.Label(info_frame, 
                          text=f"한영 간격: {spacing}초",
                          font=("Helvetica", 14), fg='white', bg='#2e2e2e')
    spacing_info.pack(side='left', padx=20)

    delay_info = tk.Label(info_frame,
                         text=f"다음 단어: {word_delay}초",
                         font=("Helvetica", 14), fg='white', bg='#2e2e2e')
    delay_info.pack(side='left', padx=20)

    def toggle_pause():
        nonlocal is_paused
        is_paused = not is_paused
        pause_button.config(text="▶️ 재생" if is_paused else "⏸️ 일시정지")

        if not is_paused and current_after_id is None:
            display_word(current_index[0])

    def display_word(index=0):
        nonlocal current_after_id
        current_index[0] = index

        if is_paused:
            current_after_id = None
            return

        if index < len(words):
            english_word, korean_meaning = words[index]
            first_word = english_word if is_eng_to_kor else korean_meaning
            second_word = korean_meaning if is_eng_to_kor else english_word
            first_lang = 'en' if is_eng_to_kor else 'ko'
            second_lang = 'ko' if is_eng_to_kor else 'en'
            first_repeat = english_repeat if is_eng_to_kor else korean_repeat
            second_repeat = korean_repeat if is_eng_to_kor else english_repeat

            print(f"\n{'='*50}")

            # 모든 텍스트 초기화 및 첫 번째 단어 표시
            label_second.config(text="")
            label_next_word.config(text="")
            label_index.config(text=f"{start_row + index}")
            label_progress.config(text=f"({index + 1}/{total_words})")
            label_first.config(text=first_word)
            root.update()

            if not is_paused:
                first_time = time.strftime('%H:%M:%S')
                print(f"[{start_row + index:03d}]")
                print(f"      [첫번째] {first_word:<25} {first_time}")
                
                for i in range(first_repeat):
                    speak(first_word, 'first_word.wav', int(speed * 300), is_korean=(first_lang=='ko'), ko_voice=ko_voice, en_voice=en_voice)
                    if i < first_repeat - 1:
                        time.sleep(0.05)
                
                # 두 번째 단어 표시
                second_time = time.strftime('%H:%M:%S')
                label_second.config(text=second_word)
                print(f"      [두번째] {second_word:<23} {second_time}")
                root.update()

                if index + 1 < len(words):
                    # 한영 모드일 때는 다음 단어를 한글로, 영한 모드일 때는 영어로 표시
                    next_word = words[index + 1][0] if is_eng_to_kor else words[index + 1][1]
                    label_next_word.config(text=f"Next: {next_word}")
                else:
                    label_next_word.config(text="마지막 단어입니다")
                root.update()

                # 두 번째 단어 발음
                for i in range(second_repeat):
                    speak(second_word, 'second_word.wav', int(speed * 300), is_korean=(second_lang=='ko'), ko_voice=ko_voice, en_voice=en_voice)
                    if i < second_repeat - 1:
                        time.sleep(0.05)

                if word_delay > 0:
                    time.sleep(word_delay)
                if not is_paused:
                    display_word(index + 1)

        else:
            label_index.config(text="끝났습니다!")
            label_progress.config(text="")
            label_first.config(text="")
            label_second.config(text="")
            label_next_word.config(text="")
            pause_button.config(state='disabled')
            print(f"\n{'='*50}")
            print(f"학습 종료 - {time.strftime('%H:%M:%S')}")
            print(f"{'='*50}")

    return_button = tk.Button(root, text="초기화면으로",
                             command=lambda: return_to_initial(root),
                             font=("Helvetica", 16), bg='#FF5722', fg='black', relief='raised',
                             activebackground='#ff7043', activeforeground='black',
                             cursor='hand2')
    return_button.place(relx=0.1, rely=0.9, anchor='w')

    pause_button = tk.Button(root, text="⏸️ 일시정지", command=toggle_pause,
                            font=("Helvetica", 20), width=10, height=1, bg='#4CAF50', fg='black', relief='raised',
                            activebackground='#45a049', activeforeground='black',
                            cursor='hand2')
    pause_button.place(relx=0.5, rely=0.9, anchor='center')

    current_index = [0]

    display_word()
    root.mainloop()

def initial_setup():
    # 저장된 설정 불러오기
    settings = load_settings()
    
    setup_root = tk.Tk()
    setup_root.title("단어 학습 설정")
    setup_root.geometry("500x800+0+0")  # 창 크기 조정
    setup_root.configure(bg='#2e2e2e')

    ttk.Label(setup_root, text="단어 학습 설정", font=("Helvetica", 32, "bold"),
              background='#2e2e2e', foreground='white').pack(pady=20)

    # 학습 방향 선택 프레임
    direction_frame = ttk.Frame(setup_root)
    direction_frame.pack(pady=10, padx=20, anchor='center')
    
    direction_var = tk.StringVar(value=settings['direction'])
    ttk.Label(direction_frame, text="학습 방향:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    
    direction_eng_to_kor = ttk.Radiobutton(direction_frame, text="영한", variable=direction_var, 
                                          value="영한", style='Custom.TRadiobutton')
    direction_eng_to_kor.pack(side='left', padx=10)
    
    direction_kor_to_eng = ttk.Radiobutton(direction_frame, text="한영", variable=direction_var, 
                                          value="한영", style='Custom.TRadiobutton')
    direction_kor_to_eng.pack(side='left', padx=10)

    # 스타일 설정
    style = ttk.Style()
    style.configure('Custom.TRadiobutton', font=('Helvetica', 20), background='#2e2e2e', foreground='white')

    # 단어 범위 프레임
    range_frame = ttk.Frame(setup_root)
    range_frame.pack(pady=10, padx=20, anchor='center')

    ttk.Label(range_frame, text="단어 범위:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').grid(row=0, column=0, padx=5, pady=5)
    start_row_entry = ttk.Entry(range_frame, font=("Helvetica", 24), justify='center', width=5)
    start_row_entry.insert(0, str(settings['start_row']))
    start_row_entry.grid(row=0, column=1, padx=5, pady=5)
    
    ttk.Label(range_frame, text="~", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').grid(row=0, column=2, padx=5, pady=5)
    end_row_entry = ttk.Entry(range_frame, font=("Helvetica", 24), justify='center', width=5)
    end_row_entry.insert(0, str(settings['end_row']))
    end_row_entry.grid(row=0, column=3, padx=5, pady=5)

    # 반복 회수 프레임
    repeat_frame = ttk.Frame(setup_root)
    repeat_frame.pack(pady=10, padx=20, anchor='center')

    ttk.Label(repeat_frame, text="영어:", font=("Helvetica", 24),
              background='#2e2e2e', foreground='white').grid(row=0, column=0, padx=5, pady=5)
    english_repeat_entry = ttk.Entry(repeat_frame, font=("Helvetica", 24), justify='center', width=5)
    english_repeat_entry.insert(0, str(settings['english_repeat']))
    english_repeat_entry.grid(row=0, column=1, padx=5, pady=5)

    ttk.Label(repeat_frame, text="한글:", font=("Helvetica", 24),
              background='#2e2e2e', foreground='white').grid(row=0, column=2, padx=5, pady=5)
    korean_repeat_entry = ttk.Entry(repeat_frame, font=("Helvetica", 24), justify='center', width=5)
    korean_repeat_entry.insert(0, str(settings['korean_repeat']))
    korean_repeat_entry.grid(row=0, column=3, padx=5, pady=5)

    # 한영 간격 프레임
    spacing_frame = ttk.Frame(setup_root)
    spacing_frame.pack(pady=10, padx=20, anchor='center')
    
    ttk.Label(spacing_frame, text="한영 간격:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    spacing_entry = ttk.Entry(spacing_frame, font=("Helvetica", 24), justify='center', width=5)
    spacing_entry.insert(0, str(settings['spacing']))
    spacing_entry.pack(side='left', padx=5)
    ttk.Label(spacing_frame, text="초", font=("Helvetica", 18),
              background='#2e2e2e', foreground='white').pack(side='left')

    # 다음 단어 프레임
    delay_frame = ttk.Frame(setup_root)
    delay_frame.pack(pady=10, padx=20, anchor='center')
    
    ttk.Label(delay_frame, text="다음 단어:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    word_delay_entry = ttk.Entry(delay_frame, font=("Helvetica", 24), justify='center', width=5)
    word_delay_entry.insert(0, str(settings['word_delay']))
    word_delay_entry.pack(side='left', padx=5)
    ttk.Label(delay_frame, text="초", font=("Helvetica", 18),
              background='#2e2e2e', foreground='white').pack(side='left')

    # 배속 프레임 추가
    speed_frame = ttk.Frame(setup_root)
    speed_frame.pack(pady=10, padx=20, anchor='center')
    
    ttk.Label(speed_frame, text="배속:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    speed_entry = ttk.Entry(speed_frame, font=("Helvetica", 24), justify='center', width=5)
    speed_entry.insert(0, str(settings['speed']))
    speed_entry.pack(side='left', padx=5)
    ttk.Label(speed_frame, text="배", font=("Helvetica", 18),
              background='#2e2e2e', foreground='white').pack(side='left')

    # 영어 음성 선택 프레임
    en_voice_frame = ttk.Frame(setup_root)
    en_voice_frame.pack(pady=10, padx=20, anchor='center')
    
    ttk.Label(en_voice_frame, text="영어 음성:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    
    en_voice_var = tk.StringVar(value=settings.get('en_voice', 'Karen'))
    en_voice_dropdown = ttk.Combobox(en_voice_frame, textvariable=en_voice_var, 
                                       values=settings.get('en_voice_list', ['Alex', 'Bruce', 'Fred', 'Richard', 'Vicki', 'Ralph', 'Thomas', 'Arthur', 'Daniel', 'John', 'Karen', 'Samantha']),
                                       state='readonly', font=("Helvetica", 20))
    en_voice_dropdown.pack(side='left', padx=5)
    
    # 한글 음성 선택 프레임
    ko_voice_frame = ttk.Frame(setup_root)
    ko_voice_frame.pack(pady=10, padx=20, anchor='center')
    
    ttk.Label(ko_voice_frame, text="한글 음성:", font=("Helvetica", 24, "bold"),
              background='#2e2e2e', foreground='white').pack(side='left', padx=5)
    
    ko_voice_var = tk.StringVar(value=settings.get('ko_voice', 'Yuna'))
    ko_voice_dropdown = ttk.Combobox(ko_voice_frame, textvariable=ko_voice_var, 
                                       values=settings.get('ko_voice_list', ['Yuna', 'Ji-eun']),
                                       state='readonly', font=("Helvetica", 20))
    ko_voice_dropdown.pack(side='left', padx=5)
    
    tk.Button(setup_root, text="시작",
              command=lambda: start_learning(start_row_entry, end_row_entry, english_repeat_entry, korean_repeat_entry,
                                           word_delay_entry, spacing_entry, speed_entry, direction_var,
                                           en_voice_var, ko_voice_var),
              font=("Helvetica", 24, "bold"), bg='#4CAF50', fg='black', width=15, height=2,
              relief='raised', bd=5, activebackground='#45a049', activeforeground='black',
              cursor='hand2').pack(pady=30)

    setup_root.mainloop()

def return_to_initial(current_window):
    current_window.destroy()
    initial_setup()

def start_learning(start_row_entry, end_row_entry, english_repeat_entry, korean_repeat_entry,
                   word_delay_entry, spacing_entry, speed_entry, direction_var, 
                   en_voice_var, ko_voice_var):
    try:
        start_row = int(start_row_entry.get())
        end_row = int(end_row_entry.get())
        english_repeat = int(english_repeat_entry.get())
        korean_repeat = int(korean_repeat_entry.get())
        word_delay = float(word_delay_entry.get())
        spacing = float(spacing_entry.get())
        speed = float(speed_entry.get())
        en_voice = en_voice_var.get()
        ko_voice = ko_voice_var.get()
        is_eng_to_kor = direction_var.get() == "영한"
        
        print(f"[DEBUG] Start Learning with English Voice: {en_voice}, Korean Voice: {ko_voice}")
        
        if not (1.0 <= speed <= 4.0):
            messagebox.showerror("입력 오류", "배속은 1.0에서 4.0 사이의 값을 입력해주세요.")
            return
            
        # 현재 설정 저장
        current_settings = {
            'start_row': start_row,
            'end_row': end_row,
            'english_repeat': english_repeat,
            'korean_repeat': korean_repeat,
            'word_delay': word_delay,
            'spacing': spacing,
            'speed': speed,
            'direction': direction_var.get(),
            'ko_voice': ko_voice,
            'en_voice': en_voice
        }
        save_settings(current_settings)

    except ValueError:
        messagebox.showerror("입력 오류", "올바른 숫자를 입력해주세요.")
        return

    start_row_entry.master.master.destroy()
    show_and_read_words(start_row, end_row, english_repeat, korean_repeat, speed, word_delay, spacing, is_eng_to_kor, en_voice, ko_voice)

if __name__ == "__main__":
    FILE_PATH = "./words.xlsx"
    initial_setup()
2

👉 이 게시글도 읽어보세요