소개
영어, 중국어, 한국어 WAV TTS, 네이버 클라우드가 유료 서비스였는데
온라인 영어, 중국어, 한국어 학원을 운영중, 영상 교재를 만들때 음성 변환이 필요
기존에는 파이썬 입코딩, 구글의 gTTS 라이브러를 통해 mp3 파일을 제작해서 사용
mp3 파일이 몸에 좋지 않은 영향을 준다는 질문을 받고, 검색을 해본결과
mbc 생명의 소리, kbs 스펀지 등 2개 프로그램에서 mp3 및 CD 등 디지탈 음성이
손실 압축방식으로 인해 몸에 아주 좋지 않는 영향을 준 다는 것을 확인하고
무손실 압축방식으로 원음과 같은 wav 파일의 존재를 확인
네이버, 아마존, 구글 등 클라우드 TTS는 WAV 파일을 지원
그래서 유로인 네이버 클라우드 서비스를 사용하여 wav로 교재를 다시 제작
네이버는 유료서비스로 api를 이용해야함
Cursor 입코딩을 하다가 pyttsx3 라는 파이썬 무료 라이브러리를 발견
진행 방법
어떤 도구를 사용했고, 어떻게 활용하셨나요?
Tip: 사용한 프롬프트 전문을 꼭 포함하고, 내용을 짧게 소개해 주세요.
Tip: 활용 이미지나 캡처 화면을 꼭 남겨주세요.
Tip: 코드 전문은 코드블록에 감싸서 작성해주세요. ( / 을 눌러 '코드 블록'을 선택)
(내용 입력)
커서를 이용 wav 제작용 파이썬 라이브러리 발견하고
커서 입코딩을 통해 파이썬으로
기존의 gTTS mp3 영어 단어장 대신
pyttsx3 wav 영어 단어장을 제작
처음에는 커서가 mp3를 wav로 변환할 것을 제안해서, 다시 질문후 차이점 다시 확인
고급음질 음성을 만드는 법을 문의한 바
pyttsx3 및 유료서비스인 아마존, 구글, IBM 등 유료 서비서를 소개
무료인 pyttsx3 라이브러리도 무손실 형식의 wav 파일을 제작 가능
결과와 배운 점
배운 점과 나만의 꿀팁을 알려주세요.
정확한 질문을 통해 무료 TTS 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()