Python 음성 합성 가젯을 구현하는 방법

WBOY
풀어 주다: 2023-05-08 15:46:07
앞으로
1752명이 탐색했습니다.

    TTS 소개

    TTS(Text To Speech)는 입력된 텍스트를 기계가 음성 형식으로 재생하여 기계가 말하는 효과를 얻을 수 있는 음성 합성 기술입니다.

    TTS는 음성 처리와 음성 합성으로 나누어집니다. 입력된 텍스트를 기계가 먼저 인식한 다음 음성 라이브러리를 기반으로 음성 합성을 수행합니다. 이제 Baidu Smart Cloud의 음성 합성 인터페이스와 같이 호출할 수 있는 TTS 인터페이스가 많이 있습니다. Microsoft는 또한 Windows 시스템에서 오프라인 TTS 음성 합성 기능을 구현하기 위해 호출할 수 있는 TTS 인터페이스를 제공합니다.

    이 기사에서는 pyttsx3 라이브러리를 데모로 사용하여 음성 합성 가젯을 작성합니다.

    필수 패키지 설치

    PyQt5 및 해당 GUI 디자인 도구 설치

    # 安装PyQt5
    pip install PyQt5
     
    # 安装PyQt5设计器
    pip install PyQt5Designer
    로그인 후 복사

    이 글에서 사용된 편집기는 PyCharm이 아닌 VSCode입니다. 실제 상황에 따라 사용 방법에 차이가 있을 수 있습니다.

    Install pyttsx3

    pip install pyttsx3
    로그인 후 복사

    UI 인터페이스

    아래 그림을 참조하여 간단한 GUI 인터페이스를 디자인할 수 있습니다. 이 글은 주로 기능적인 예시이므로 인터페이스의 미학은 고려하지 않습니다.

    Python 음성 합성 가젯을 구현하는 방법

    인터페이스에는 음성으로 변환할 텍스트를 입력하기 위한 텍스트 입력 상자와 음성 재생 방법을 실행하는 재생 버튼이 있어야 합니다. 필요에 따라 음성 속도, 볼륨, 언어를 선택할 수 있습니다.

    PyQt5 디자인 도구를 사용하면 위에서 구성한 GUI 인터페이스를 기반으로 다음과 같은 UI(XML) 코드를 생성할 수 있습니다.

    <?xml version="1.0" encoding="UTF-8"?>
    <ui version="4.0">
     <class>Form</class>
     <widget class="QWidget" name="Form">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>313</width>
        <height>284</height>
       </rect>
      </property>
      <property name="windowTitle">
       <string>语音合成器</string>
      </property>
      <property name="windowIcon">
       <iconset>
        <normaloff>voice.ico</normaloff>voice.ico</iconset>
      </property>
      <widget class="QWidget" name="verticalLayoutWidget">
       <property name="geometry">
        <rect>
         <x>10</x>
         <y>10</y>
         <width>291</width>
         <height>261</height>
        </rect>
       </property>
       <layout class="QVBoxLayout" name="verticalLayout">
        <property name="spacing">
         <number>20</number>
        </property>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_2">
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <string>播报文本</string>
            </property>
            <property name="alignment">
             <set>Qt::AlignJustify|Qt::AlignTop</set>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QTextEdit" name="tbx_text"/>
          </item>
         </layout>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_4">
          <item>
           <widget class="QLabel" name="label_3">
            <property name="text">
             <string>语速</string>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QSlider" name="slider_rate">
            <property name="maximum">
             <number>300</number>
            </property>
            <property name="orientation">
             <enum>Qt::Horizontal</enum>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QLabel" name="label_rate">
            <property name="minimumSize">
             <size>
              <width>30</width>
              <height>0</height>
             </size>
            </property>
            <property name="text">
             <string>0</string>
            </property>
            <property name="alignment">
             <set>Qt::AlignCenter</set>
            </property>
           </widget>
          </item>
         </layout>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_3">
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
             <string>音量</string>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QSlider" name="slider_volumn">
            <property name="maximum">
             <number>100</number>
            </property>
            <property name="orientation">
             <enum>Qt::Horizontal</enum>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QLabel" name="label_volumn">
            <property name="minimumSize">
             <size>
              <width>30</width>
              <height>0</height>
             </size>
            </property>
            <property name="text">
             <string>0</string>
            </property>
            <property name="alignment">
             <set>Qt::AlignCenter</set>
            </property>
           </widget>
          </item>
         </layout>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout">
          <item>
           <widget class="QLabel" name="label_4">
            <property name="text">
             <string>选择语言</string>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QRadioButton" name="rbtn_zh">
            <property name="text">
             <string>中文</string>
            </property>
            <property name="checked">
             <bool>true</bool>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QRadioButton" name="rbtn_en">
            <property name="text">
             <string>英文</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_5">
          <item>
           <widget class="QLabel" name="label_5">
            <property name="minimumSize">
             <size>
              <width>60</width>
              <height>0</height>
             </size>
            </property>
            <property name="text">
             <string/>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QPushButton" name="btn_play">
            <property name="minimumSize">
             <size>
              <width>0</width>
              <height>30</height>
             </size>
            </property>
            <property name="text">
             <string>播放</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
     </widget>
     <resources/>
     <connections/>
    </ui>
    로그인 후 복사

    마지막으로 PyQt5 인터페이스 도구를 사용하면 위 UI 코드를 기반으로 다음과 같은 폼 클래스를 생성할 수 있습니다. :

    # -*- coding: utf-8 -*-
     
    # Form implementation generated from reading ui file &#39;d:\Program\VSCode\Python\TTS_PyQT\tts_form.ui&#39;
    #
    # Created by: PyQt5 UI code generator 5.15.7
    #
    # WARNING: Any manual changes made to this file will be lost when pyuic5 is
    # run again.  Do not edit this file unless you know what you are doing.
     
    from PyQt5 import QtCore, QtGui, QtWidgets
     
     
    class Ui_Form(object):
     
        def setupUi(self, Form):
            Form.setObjectName("Form")
            Form.resize(313, 284)
            icon = QtGui.QIcon()
            icon.addPixmap(
                QtGui.QPixmap("./voice.ico"),
                QtGui.QIcon.Normal, QtGui.QIcon.Off)
            Form.setWindowIcon(icon)
            self.verticalLayoutWidget = QtWidgets.QWidget(Form)
            self.verticalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 291, 261))
            self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
            self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
            self.verticalLayout.setContentsMargins(0, 0, 0, 0)
            self.verticalLayout.setSpacing(20)
            self.verticalLayout.setObjectName("verticalLayout")
            self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
            self.horizontalLayout_2.setObjectName("horizontalLayout_2")
            self.label = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label.setAlignment(QtCore.Qt.AlignJustify | QtCore.Qt.AlignTop)
            self.label.setObjectName("label")
            self.horizontalLayout_2.addWidget(self.label)
            self.tbx_text = QtWidgets.QTextEdit(self.verticalLayoutWidget)
            self.tbx_text.setObjectName("tbx_text")
            self.horizontalLayout_2.addWidget(self.tbx_text)
            self.verticalLayout.addLayout(self.horizontalLayout_2)
            self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
            self.horizontalLayout_4.setObjectName("horizontalLayout_4")
            self.label_3 = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_3.setObjectName("label_3")
            self.horizontalLayout_4.addWidget(self.label_3)
            self.slider_rate = QtWidgets.QSlider(self.verticalLayoutWidget)
            self.slider_rate.setMaximum(300)
            self.slider_rate.setOrientation(QtCore.Qt.Horizontal)
            self.slider_rate.setObjectName("slider_rate")
            self.horizontalLayout_4.addWidget(self.slider_rate)
            self.label_rate = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_rate.setMinimumSize(QtCore.QSize(30, 0))
            self.label_rate.setAlignment(QtCore.Qt.AlignCenter)
            self.label_rate.setObjectName("label_rate")
            self.horizontalLayout_4.addWidget(self.label_rate)
            self.verticalLayout.addLayout(self.horizontalLayout_4)
            self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
            self.horizontalLayout_3.setObjectName("horizontalLayout_3")
            self.label_2 = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_2.setObjectName("label_2")
            self.horizontalLayout_3.addWidget(self.label_2)
            self.slider_volumn = QtWidgets.QSlider(self.verticalLayoutWidget)
            self.slider_volumn.setMaximum(100)
            self.slider_volumn.setOrientation(QtCore.Qt.Horizontal)
            self.slider_volumn.setObjectName("slider_volumn")
            self.horizontalLayout_3.addWidget(self.slider_volumn)
            self.label_volumn = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_volumn.setMinimumSize(QtCore.QSize(30, 0))
            self.label_volumn.setAlignment(QtCore.Qt.AlignCenter)
            self.label_volumn.setObjectName("label_volumn")
            self.horizontalLayout_3.addWidget(self.label_volumn)
            self.verticalLayout.addLayout(self.horizontalLayout_3)
            self.horizontalLayout = QtWidgets.QHBoxLayout()
            self.horizontalLayout.setObjectName("horizontalLayout")
            self.label_4 = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_4.setObjectName("label_4")
            self.horizontalLayout.addWidget(self.label_4)
            self.rbtn_zh = QtWidgets.QRadioButton(self.verticalLayoutWidget)
            self.rbtn_zh.setChecked(True)
            self.rbtn_zh.setObjectName("rbtn_zh")
            self.horizontalLayout.addWidget(self.rbtn_zh)
            self.rbtn_en = QtWidgets.QRadioButton(self.verticalLayoutWidget)
            self.rbtn_en.setObjectName("rbtn_en")
            self.horizontalLayout.addWidget(self.rbtn_en)
            self.verticalLayout.addLayout(self.horizontalLayout)
            self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
            self.horizontalLayout_5.setObjectName("horizontalLayout_5")
            self.label_5 = QtWidgets.QLabel(self.verticalLayoutWidget)
            self.label_5.setMinimumSize(QtCore.QSize(60, 0))
            self.label_5.setText("")
            self.label_5.setObjectName("label_5")
            self.horizontalLayout_5.addWidget(self.label_5)
            self.btn_play = QtWidgets.QPushButton(self.verticalLayoutWidget)
            self.btn_play.setMinimumSize(QtCore.QSize(0, 30))
            self.btn_play.setObjectName("btn_play")
            self.horizontalLayout_5.addWidget(self.btn_play)
            self.verticalLayout.addLayout(self.horizontalLayout_5)
     
            self.retranslateUi(Form)
            QtCore.QMetaObject.connectSlotsByName(Form)
     
        def retranslateUi(self, Form):
            _translate = QtCore.QCoreApplication.translate
            Form.setWindowTitle(_translate("Form", "语音合成器"))
            self.label.setText(_translate("Form", "播报文本"))
            self.label_3.setText(_translate("Form", "语速"))
            self.label_rate.setText(_translate("Form", "0"))
            self.label_2.setText(_translate("Form", "音量"))
            self.label_volumn.setText(_translate("Form", "0"))
            self.label_4.setText(_translate("Form", "选择语言"))
            self.rbtn_zh.setText(_translate("Form", "中文"))
            self.rbtn_en.setText(_translate("Form", "英文"))
            self.btn_play.setText(_translate("Form", "播放"))
    로그인 후 복사

    본 코드를 직접 복사하시면 아이콘이 사라질 수 있습니다. 이를 위해서는 실제 상황에 맞게 아이콘 구성을 수정하고 사용할 ico 아이콘 파일을 추가해야 합니다.

    함수 코드

    음성 도구 클래스

    먼저 음성 합성을 위한 음성 엔진 개체를 초기화하고 가져와야 합니다.

    # tts对象
    engine = pyttsx3.init()
    로그인 후 복사

    객체의 setProperty 메소드를 통해 음성 합성 객체의 속성을 수정할 수 있습니다.

    Property nameExplanation
    rate분당 단어로 표현되는 정수 말하기 속도
    VolumeVolume, 값 범위는 [0.0, 1.0]
    voicesvoice

    의 문자열 식별자입니다. 음성 도구 클래스 코드는 다음과 같습니다. 코드의 의미는 주석을 참조할 수 있습니다.

    import pyttsx3
     
     
    class VoiceEngine():
        &#39;&#39;&#39;
        tts 语音工具类
        &#39;&#39;&#39;
     
        def __init__(self):
            &#39;&#39;&#39;
            初始化
            &#39;&#39;&#39;
            # tts对象
            self.__engine = pyttsx3.init()
            # 语速
            self.__rate = 150
            # 音量
            self.__volume = 100
            # 语音ID,0为中文,1为英文
            self.__voice = 0
     
        @property
        def Rate(self):
            &#39;&#39;&#39;
            语速属性
            &#39;&#39;&#39;
            return self.__rate
     
        @Rate.setter
        def Rate(self, value):
            self.__rate = value
     
        @property
        def Volume(self):
            &#39;&#39;&#39;
            音量属性
            &#39;&#39;&#39;
            return self.__volume
     
        @Volume.setter
        def Volume(self, value):
            self.__volume = value
     
        @property
        def VoiceID(self):
            &#39;&#39;&#39;
            语音ID:0 -- 中文;1 -- 英文
            &#39;&#39;&#39;
     
            return self.__voice
     
        @VoiceID.setter
        def VoiceID(self, value):
            self.__voice = value
     
        def Say(self, text):
            &#39;&#39;&#39;
            播放语音
            &#39;&#39;&#39;
            self.__engine.setProperty(&#39;rate&#39;, self.__rate)
            self.__engine.setProperty(&#39;volume&#39;, self.__volume)
     
            # 获取可用语音列表,并设置语音
            voices = self.__engine.getProperty(&#39;voices&#39;)
            self.__engine.setProperty(&#39;voice&#39;, voices[self.__voice].id)
     
            # 保存语音文件
            # self.__engine.save_to_file(text, &#39;test.mp3&#39;)
     
            self.__engine.say(text)
            self.__engine.runAndWait()
            self.__engine.stop()
    로그인 후 복사

    Window Body 클래스

    방금 생성한 PyQt5를 상속하는 폼 클래스를 생성하고, 폼의 드래그 및 클릭 이벤트에 대한 콜백 함수를 등록하고, 지정된 이벤트를 트리거하는 음성 도구 클래스의 인스턴스를 생성할 수 있습니다. . 수행해야 할 음성 작업입니다.

    import sys
    import _thread as th
    from PyQt5.QtWidgets import QMainWindow, QApplication
    from Ui_tts_form import Ui_Form
     
    class MainWindow(QMainWindow, Ui_Form):
        &#39;&#39;&#39;
        窗体类
        &#39;&#39;&#39;
     
        def __init__(self, parent=None):
            &#39;&#39;&#39;
            初始化窗体
            &#39;&#39;&#39;
            super(MainWindow, self).__init__(parent)
            self.setupUi(self)
     
            # 获取tts工具类实例
            self.engine = VoiceEngine()
            self.__isPlaying = False
     
            # 设置初始文本
            self.tbx_text.setText(&#39;床前明月光,疑似地上霜。\n举头望明月,低头思故乡。&#39;)
     
            # 进度条数据绑定到label中显示
            self.slider_rate.valueChanged.connect(self.setRateTextValue)
            self.slider_volumn.valueChanged.connect(self.setVolumnTextValue)
     
            # 设置进度条初始值
            self.slider_rate.setValue(self.engine.Rate)
            self.slider_volumn.setValue(self.engine.Volume)
     
            # RadioButton选择事件
            self.rbtn_zh.toggled.connect(self.onSelectVoice_zh)
            self.rbtn_en.toggled.connect(self.onSelectVoice_en)
     
            # 播放按钮点击事件
            self.btn_play.clicked.connect(self.onPlayButtonClick)
     
        def setRateTextValue(self):
            &#39;&#39;&#39;
            修改语速label文本值
            &#39;&#39;&#39;
            value = self.slider_rate.value()
            self.label_rate.setText(str(value))
            self.engine.Rate = value
     
        def setVolumnTextValue(self):
            &#39;&#39;&#39;
            修改音量label文本值
            &#39;&#39;&#39;
            value = self.slider_volumn.value()
            self.label_volumn.setText(str(value / 100))
            self.engine.Volume = value
     
        def onSelectVoice_zh(self):
            &#39;&#39;&#39;
            修改中文的语音配置及默认播放文本
            &#39;&#39;&#39;
            self.tbx_text.setText(&#39;床前明月光,疑似地上霜。\n举头望明月,低头思故乡。&#39;)
            self.engine.VoiceID = 0
     
        def onSelectVoice_en(self):
            &#39;&#39;&#39;
            修改英文的语音配置及默认的播放文本
            &#39;&#39;&#39;
            self.tbx_text.setText(&#39;Hello World&#39;)
            self.engine.VoiceID = 1
     
        def playVoice(self):
            &#39;&#39;&#39;
            播放
            &#39;&#39;&#39;
     
            if self.__isPlaying is not True:
                self.__isPlaying = True
                text = self.tbx_text.toPlainText()
                self.engine.Say(text)
                self.__isPlaying = False
     
        def onPlayButtonClick(self):
            &#39;&#39;&#39;
            播放按钮点击事件
            开启线程新线程播放语音,避免窗体因为语音播放而假卡死
            &#39;&#39;&#39;
            th.start_new_thread(self.playVoice, ())
    로그인 후 복사

    전체 코드

    import sys
    import _thread as th
    from PyQt5.QtWidgets import QMainWindow, QApplication
    from Ui_tts_form import Ui_Form
    import pyttsx3
     
     
    class VoiceEngine():
        &#39;&#39;&#39;
        tts 语音工具类
        &#39;&#39;&#39;
     
        def __init__(self):
            &#39;&#39;&#39;
            初始化
            &#39;&#39;&#39;
            # tts对象
            self.__engine = pyttsx3.init()
            # 语速
            self.__rate = 150
            # 音量
            self.__volume = 100
            # 语音ID,0为中文,1为英文
            self.__voice = 0
     
        @property
        def Rate(self):
            &#39;&#39;&#39;
            语速属性
            &#39;&#39;&#39;
            return self.__rate
     
        @Rate.setter
        def Rate(self, value):
            self.__rate = value
     
        @property
        def Volume(self):
            &#39;&#39;&#39;
            音量属性
            &#39;&#39;&#39;
            return self.__volume
     
        @Volume.setter
        def Volume(self, value):
            self.__volume = value
     
        @property
        def VoiceID(self):
            &#39;&#39;&#39;
            语音ID:0 -- 中文;1 -- 英文
            &#39;&#39;&#39;
     
            return self.__voice
     
        @VoiceID.setter
        def VoiceID(self, value):
            self.__voice = value
     
        def Say(self, text):
            &#39;&#39;&#39;
            播放语音
            &#39;&#39;&#39;
            self.__engine.setProperty(&#39;rate&#39;, self.__rate)
            self.__engine.setProperty(&#39;volume&#39;, self.__volume)
            voices = self.__engine.getProperty(&#39;voices&#39;)
            self.__engine.setProperty(&#39;voice&#39;, voices[self.__voice])
     
            # 保存语音文件
            # self.__engine.save_to_file(text, &#39;test.mp3&#39;)
     
            self.__engine.say(text)
            self.__engine.runAndWait()
            self.__engine.stop()
     
     
    class MainWindow(QMainWindow, Ui_Form):
        &#39;&#39;&#39;
        窗体类
        &#39;&#39;&#39;
     
        def __init__(self, parent=None):
            &#39;&#39;&#39;
            初始化窗体
            &#39;&#39;&#39;
            super(MainWindow, self).__init__(parent)
            self.setupUi(self)
     
            # 获取tts工具类实例
            self.engine = VoiceEngine()
            self.__isPlaying = False
     
            # 设置初始文本
            self.tbx_text.setText(&#39;床前明月光,疑似地上霜。\n举头望明月,低头思故乡。&#39;)
     
            # 进度条数据绑定到label中显示
            self.slider_rate.valueChanged.connect(self.setRateTextValue)
            self.slider_volumn.valueChanged.connect(self.setVolumnTextValue)
     
            # 设置进度条初始值
            self.slider_rate.setValue(self.engine.Rate)
            self.slider_volumn.setValue(self.engine.Volume)
     
            # RadioButton选择事件
            self.rbtn_zh.toggled.connect(self.onSelectVoice_zh)
            self.rbtn_en.toggled.connect(self.onSelectVoice_en)
     
            # 播放按钮点击事件
            self.btn_play.clicked.connect(self.onPlayButtonClick)
     
        def setRateTextValue(self):
            &#39;&#39;&#39;
            修改语速label文本值
            &#39;&#39;&#39;
            value = self.slider_rate.value()
            self.label_rate.setText(str(value))
            self.engine.Rate = value
     
        def setVolumnTextValue(self):
            &#39;&#39;&#39;
            修改音量label文本值
            &#39;&#39;&#39;
            value = self.slider_volumn.value()
            self.label_volumn.setText(str(value / 100))
            self.engine.Volume = value
     
        def onSelectVoice_zh(self):
            &#39;&#39;&#39;
            修改中文的语音配置及默认播放文本
            &#39;&#39;&#39;
            self.tbx_text.setText(&#39;床前明月光,疑似地上霜。\n举头望明月,低头思故乡。&#39;)
            self.engine.VoiceID = 0
     
        def onSelectVoice_en(self):
            &#39;&#39;&#39;
            修改英文的语音配置及默认的播放文本
            &#39;&#39;&#39;
            self.tbx_text.setText(&#39;Hello World&#39;)
            self.engine.VoiceID = 1
     
        def playVoice(self):
            &#39;&#39;&#39;
            播放
            &#39;&#39;&#39;
     
            if self.__isPlaying is not True:
                self.__isPlaying = True
                text = self.tbx_text.toPlainText()
                self.engine.Say(text)
                self.__isPlaying = False
     
        def onPlayButtonClick(self):
            &#39;&#39;&#39;
            修改语速label文本值
            &#39;&#39;&#39;
            th.start_new_thread(self.playVoice, ())
     
     
    if __name__ == "__main__":
        &#39;&#39;&#39;
        主函数
        &#39;&#39;&#39;
        app = QApplication(sys.argv)
        form = MainWindow()
        form.show()
        sys.exit(app.exec_())
    로그인 후 복사

    위 내용은 Python 음성 합성 가젯을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    관련 라벨:
    원천:yisu.com
    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
    인기 튜토리얼
    더>
    최신 다운로드
    더>
    웹 효과
    웹사이트 소스 코드
    웹사이트 자료
    프론트엔드 템플릿