import time
import obsws_python as obs
import json
from pprint import pprint
from Module.print_color import print_color
from langdetect import detect
from langdetect.lang_detect_exception import LangDetectException

class obs_socket():
    def __init__(self):
        # OBSの接続設定
        self.host = "localhost"
        self.port = 4455
        self.password = "nDCgoGVKLiJBKKRr"
        self.request = obs.ReqClient(host=self.host, port=self.port, password=self.password)
        self.event = obs.EventClient(host=self.host, port=self.port, password=self.password)
        self.Display_size =[]

    def get_settings(self):
        #画面サイズの取得
        self.Display_size=[self.request.get_video_settings().base_width,self.request.get_video_settings().base_height]
        print(f"DisplaySize: {self.Display_size}")

    def GetSceneList(self):
        #Return Example
        #{'current_preview_scene_name': None, 'current_program_scene_name': 'scene1', 'scenes': [{'sceneIndex': 0, 'sceneName': 'scene2'}, {'sceneIndex': 1, 'sceneName': 'scene1'}]}
        mydirect=self.request.get_scene_list()
        results={}
        for item in mydirect.attrs():
            results[item] = getattr(mydirect,item)  # 各属性の値を辞書に保存します。
        return results

    def GetSceneItemList(self):
        mydirect=self.request.get_scene_item_list("scene1")
        print("GetSceneItemList:")
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def GetInputKindList(self,unversioned=True):
        mydirect=self.request.get_input_kind_list(unversioned)
        print("GetInputKindList:")
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
        return getattr(mydirect,item)
    
    def GetInputDefaultSettings(self,inputName):
        mydirect=self.request.get_input_default_settings(inputName)
        print("GetInputDefaultSettings:")
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def GetInputSettings(self,inputName):
        mydirect=self.request.get_input_settings(inputName)
        print("GetInputSettings:")
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def SetInputSettings(self,inputName,inputSettings,overlay=True):
        self.request.set_input_settings(name=inputName,settings=inputSettings,overlay=overlay)
    
    def GetSceneItemId(self,scene_name,source_name):
        mydirect=self.request.get_scene_item_id(scene_name=scene_name,source_name=source_name)
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
        return getattr(mydirect,item)
    
    def GetSceneItemTransform(self,scene_name,item_id):
        mydirect=self.request.get_scene_item_transform(scene_name=scene_name,item_id=item_id)
        print("GetSceneItemTransform:")
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def GetSceneItemIndex(self,scene_name,item_id):
        mydirect=self.request.get_scene_item_index(scene_name=scene_name,item_id=item_id)
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def GetSourceFilterList(self,source_name):
        mydirect=self.request.get_source_filter_list(source_name)
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    def GetSourceFilterDefaultSettings(self,filterKind):
        mydirect=self.request.get_source_filter_default_settings(filterKind)
        for item in mydirect.attrs():
            print(f"{item}:")
            pprint(getattr(mydirect,item))
    
    #Record Request
    def StartRecord(self):
        self.request.start_record()
    
    def StopRecord(self):
        self.request.stop_record()
    
    def text_change(self,inputName,inputSettings,overlay,sceneName = 'auto',fontsize=48,bk_color=0,bk_opacity=0,subDisplayAnchor='bottom',subDisplayOffset = 0,lineBreakTextMax_jp=70,lineBreakTextMax_en=102):
        #フォント等設定 カラーはXlRgbColorで指定する。
        TextBoxSetting_JP={  'align': 'left',
                    'antialiasing': True,
                    'bk_color': bk_color,
                    'bk_opacity': bk_opacity,
                    'chatlog_lines': 6,
                    'color': 16777215,
                    'extents_cx': 100,
                    'extents_cy': 100,
                    'extents_wrap': True,
                    'font': {'face': 'コーポレート・ロゴ ver3 Medium', 'size': fontsize},
                    'gradient_color': 16777215,
                    'gradient_dir': 90.0,
                    'gradient_opacity': 100,
                    'opacity': 100,
                    'outline_color': 16777215,
                    'outline_opacity': 100,
                    'outline_size': 2,
                    'transform': 0,
                    'valign': 'top'}
        
        TextBoxSetting_EN={  'align': 'left',
                    'antialiasing': True,
                    'bk_color': bk_color,
                    'bk_opacity': bk_opacity,
                    'chatlog_lines': 6,
                    'color': 16777215,
                    'extents_cx': 100,
                    'extents_cy': 100,
                    'extents_wrap': True,
                    'font': {'face': 'Arial', 'size': fontsize},
                    'gradient_color': 16777215,
                    'gradient_dir': 90.0,
                    'gradient_opacity': 100,
                    'opacity': 100,
                    'outline_color': 16777215,
                    'outline_opacity': 100,
                    'outline_size': 2,
                    'transform': 0,
                    'valign': 'top'}
        
        #設定されたボックステキストが同一か確認
        if inputSettings == str(self.request.get_input_settings(inputName).input_settings):
            time.sleep(0.1)
            return
        #画面サイズを取得しているか確認
        if self.Display_size==[]:
            self.get_settings()
        
        #シーン名を取得
        if sceneName=='auto':
            scenelist=self.GetSceneList()
            sceneName=scenelist['current_program_scene_name']

        #テキストボックスのidを取得
        try:
            item_id=self.request.get_scene_item_id(scene_name=sceneName,source_name=inputName).scene_item_id
        except obs.error.OBSSDKError as e:
            print_color(f"OBSSDKError: {e}\n[字幕は更新されませんでした。]")
            return
        #print("Transform:")
        #print(self.request.get_scene_item_transform(scene_name=sceneName,item_id=item_id).scene_item_transform)

        
        #言語を判定し言語によりテキスト設定の変更
        try:
            if detect(inputSettings["text"]) == 'ja':
                lineBreakTextMax=lineBreakTextMax_jp
                TextBoxSetting = TextBoxSetting_JP
            else:
                lineBreakTextMax=lineBreakTextMax_en
                TextBoxSetting = TextBoxSetting_EN
        except LangDetectException as e:
            print_color(f"[OBS]Error detecting language: {e}\nInput Text:{inputSettings['text']}")
            # エラー発生時のデフォルト設定
            lineBreakTextMax=lineBreakTextMax_en
            TextBoxSetting = TextBoxSetting_EN\

        #文章長さを取得し長い場合改行する
        result = ""
        if len(inputSettings["text"])>= lineBreakTextMax:
            text = inputSettings["text"]
            for i in range(0, len(text), lineBreakTextMax):
                result += text[i:i+lineBreakTextMax]
                if i + lineBreakTextMax < len(text):
                    result += "\n"
            inputSettings["text"] = result
        
        inputSettings.update(TextBoxSetting)
        #print(f"Inputs:{inputSettings}")
        #テキストボックスを変更する
        self.request.set_input_settings(name=inputName,settings=inputSettings,overlay=overlay)
        BR_count = result.count('\n')+1
        positonY = 0
        #テキストボックス位置を変更
        if subDisplayAnchor == 'bottom':
            positonY = (self.Display_size[1] - (BR_count*fontsize) - subDisplayOffset).__round__(3)
        else:
            positonY = subDisplayOffset
        self.request.set_scene_item_transform(scene_name=sceneName,item_id=item_id,transform={'scaleX': 1,'scaleY': 1,'positionY':positonY})
        #print("after:")
        #pprint(self.request.get_scene_item_transform(scene_name=sceneName,item_id=item_id).scene_item_transform)

if __name__ == "__main__":
    obsclass=obs_socket()
    obsclass.get_settings()
    obsclass.GetSceneList()
    text_JP = """Aiユーチューバーって面白いよね。最近は技術が進化して、もう人間と見分けが付かないくらいリアルに動いたり喋ったりするよ！自分で画面の中で色々なことをして、みんなに笑いを提供できるなんてすごいことだよね。ありがと、博士！これからも面白い話題を提供できるように頑張るね！"""
    text_EN = '''Ai YouTubers are very interesting, aren't they? Technology has evolved so much recently that they can move and talk so realistically that you can hardly distinguish them from human beings! It's amazing how you can do things on screen and make people laugh. Thank you, Doctor! I will keep trying my best to provide interesting topics!'''
    obsclass.text_change(inputName="subs",inputSettings={"text":text_JP},overlay=False)
    time.sleep(3)
    obsclass.text_change(inputName="subs",inputSettings={"text":text_EN},overlay=False)
    
"""
    text=""
    for count in range(50):
        text+=str(count)
        obsclass.text_change(sceneName="scene1",inputName="sub_text",inputSettings={"text":text},overlay=False)
    for i in range(len(text), 0, -1):
        text = text[:i]
        obsclass.text_change(sceneName="scene1",inputName="sub_text",inputSettings={"text":text},overlay=False)
        """
Module/OBS_plugin_5.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
import asyncio
import websockets
import json
from PIL import Image
import base64
import io
import os
from pprint import pprint
import time
import csv

class VTS_main:
    def __init__(self):
        self.uri = "ws://localhost:8001"
        self.current_directory = os.path.dirname(os.path.abspath(__file__))
        self.pluginName="AITuber_API"
        self.pluginDeveloper="Gorilla"
        self.api_ver="1.0" #配布されているAPIのバージョン。基本変更禁止
        self.api_name="VTubeStudioPublicAPI" #同上
        self.key_check=False

        #設定ファイルの読み込み
        data = {}
        current_key = ""
        self.setting_file_path= os.path.join(self.current_directory,"VtuberStudio_plugin","setting.txt")
        with open(self.setting_file_path, 'r', encoding='utf-8') as file:
            for line in file:
                if line.strip().endswith(":"):
                    current_key = line.strip()[:-1]
                    data[current_key] = ""
                else:
                    data[current_key] += line.strip() + "\n"
        for key in data.keys():
            data[key] = data[key].strip()
        if data['token'] =="":
            self.VTS_token= None
        else:
            self.VTS_token = data['token']

    #アニメーション用 easeBothイメージング関数
    def easeInOutCubic(self,a, b, c, d):
        a /= d / 2
        if a < 1:
            result = c / 2 * a * a * a + b
        else:
            a -= 2
            result = c / 2 * (a * a * a + 2) + b
        return round(result, 1)

    def encode_image(self,file_path, target_size=(128, 128)):
        #アイコン用の画像ファイルを開き変換して返す。
        try:
            # 画像を開く
            image = Image.open(file_path)

            # 画像の形式を確認 (PNG または JPEG)
            if image.format not in ['PNG', 'JPEG']:
                raise ValueError("画像形式がPNGまたはJPGではありません。")

            # 画像をリサイズ
            resized_image = image.resize(target_size)

            # 画像をバイナリ形式で読み込む
            buffered = io.BytesIO()
            resized_image.save(buffered, format="PNG")
            buffered.seek(0)

            # 画像をBase64エンコード
            img_base64 = base64.b64encode(buffered.read()).decode("utf-8")

            return img_base64
        except Exception as e:
            print("アイコン取得エラー:", e)
            return None

    async def send_request(websocket, request):
        #websocketの送受信用
        try:
            await websocket.send(json.dumps(request))
            response_str = await websocket.recv()
            response = json.loads(response_str)
            formatted_response = json.dumps(response, indent=2)
            print(f"受信データ ({request['messageType']}):\n{formatted_response}")
        except Exception as e:
            print(f"リクエスト ({request['messageType']}) でエラーが発生しました: {e}")
        return response

    async def API_Server_Discovery(self,websocket,requestID="MyIDWithLessThan64Characters1"):
        #VTSのバージョン確認
        request = {
                "apiName": "VTubeStudioPublicAPI",
                "apiVersion": "1.0",
                "requestID": requestID,
                "messageType": "APIStateRequest"
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        
        if json_response["requestID"] == requestID:
            self.api_ver = json_response["apiVersion"]
            self.api_name = json_response["apiName"]
            return {"timestamp":json_response["timestamp"],
                    "messageType":json_response["messageType"],
                    "active":json_response["data"]["active"],
                    "vTubeStudioVersion":json_response["data"]["vTubeStudioVersion"],
                    "currentSessionAuthenticated":json_response["data"]["currentSessionAuthenticated"]}
        """
        Response Example:
        {
        "apiName": "VTubeStudioPublicAPI",
        "apiVersion": "1.0",
        "timestamp": 1625405710728,
        "messageType": "APIStateResponse",
        "requestID": "MyIDWithLessThan64Characters",
        "data": {
            "active": true,
            "vTubeStudioVersion": "1.9.0",
            "currentSessionAuthenticated": false
        }
        }
        """

    async def Requesting_list_of_hotkeys(self,websocket,requestID="MyIDWithLessThan64Characters1"):
        #13:現行モデルまたは他のVTSモデルで使用可能なホットキーのリストを要求する
        #本コードは複数モデル非対応
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "HotkeysInCurrentModelRequest"
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response) 
        available_hotkeys = json_response['data']['availableHotkeys']
        return available_hotkeys
        keys = {}

        for hotkey in available_hotkeys:
            name = hotkey['name']
            hotkey_id = hotkey['hotkeyID']
            keys[name] = hotkey_id

        print("現行モデルまたは他のVTSモデルで使用可能なホットキーのリスト/\nname : hotkeyID")
        pprint(keys)
        return keys

        """ Examples:
        "availableHotkeys": [
            {
                "name": "My Animation 1",
                "type": "TriggerAnimation",
                "description": "Triggers an animation",
                "file": "hiyori_m01.motion3.json",
                "hotkeyID": "14d17d37e79b4ed0a3c640acba1b95db",
                "keyCombination": [],
                "onScreenButtonID": 1
            },
            {
                "name": "My Animation 2",
                "type": "TriggerAnimation",
                "description": "Triggers an animation",
                "file": "hiyori_m02.motion3.json",
                "hotkeyID": "533fd7c12f934eaba6c5094427590efd",
                "keyCombination": [],
                "onScreenButtonID": 2
            },
            {
                "name": "My Animation 3",
                "type": "TriggerAnimation",
                "description": "Triggers an animation",
                "file": "hiyori_m03.motion3.json",
                "hotkeyID": "2729934fd6cd46af899281ae3e57092c",
                "keyCombination": [],
                "onScreenButtonID": 3
            },
            {
                "name": "My Animation 4",
                "type": "TriggerAnimation",
                "description": "Triggers an animation",
                "file": "hiyori_m04.motion3.json",
                "hotkeyID": "362ccfbb9139465fbf1fa1173563a3e6",
                "keyCombination": [],
                "onScreenButtonID": 4
            }
        """
    
    async def Requesting_execution_of_hotkeys(self,websocket,hotkeyID,itemInstanceID="",requestID="MyIDWithLessThan64Characters1"):
        #12:ホットキーの実行を要求する
        #本コードは複数モデル非対応
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "HotkeyTriggerRequest",
            "data": {
                "hotkeyID": hotkeyID,
                "itemInstanceID": itemInstanceID
                }
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response) 
        print(f"ホットキー{hotkeyID}の実行を要求")
        #print(json.dumps(json_response,indent=4))

        """ Examples:
        """

    async def Requesting_activation_or_deactivation_of_expressions(self,websocket,expressionFile,active=True,requestID="MyIDWithLessThan64Characters1"):
        #14:表情の活性化・非活性化の要求
        #本コードは複数モデル非対応 非推奨コード
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "ExpressionActivationRequest",
            "data": {
                "expressionFile": expressionFile,
                "active": active
            }
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        if active: 
            print(f"表情 {expressionFile}を活性化")
        else:
            print(f"表情 {expressionFile}を非活性化")
        #print(json.dumps(json_response,indent=4))

        """ Examples:
        """

    async def Requesting_current_expression_state_list(self,websocket,expressionFile="",requestID="MyIDWithLessThan64Characters1"):
        #13:現在の表現状態を要求する
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "ExpressionStateRequest",
            "data": {
                "details": False,
                "expressionFile": expressionFile
                }
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        expression_result = json_response['data']['expressions']
        #print("現在の表現状態/") 
        result = []
        for dictionary in expression_result:
            add_data = {}
            for name, value in dictionary.items():
                if name == "name" or name== "file" or name== "active":
                    add_data[name]=value
            result.append(add_data)
        #pprint(result)
        return expression_result
        print(json.dumps(json_response,indent=4))

        """Example:
        "expressions": [
            {
                "name": "EyesCry",
                "file": "EyesCry.exp3.json",
                "active": false,
                "deactivateWhenKeyIsLetGo": false,
                "autoDeactivateAfterSeconds": false,
                "secondsRemaining": 0.0,
                "secondsSinceLastActive": 0.0,
                "usedInHotkeys": [],
                "parameters": []
            },
            {
                "name": "EyesLove",
                "file": "EyesLove.exp3.json",
                "active": true,
                "deactivateWhenKeyIsLetGo": false,
                "autoDeactivateAfterSeconds": false,
                "secondsRemaining": 0.0,
                "secondsSinceLastActive": 0.0,
                "usedInHotkeys": [],
                "parameters": []
            },
            {
                "name": "SignAngry",
                "file": "SignAngry.exp3.json",
                "active": true,
                "deactivateWhenKeyIsLetGo": false,
                "autoDeactivateAfterSeconds": false,
                "secondsRemaining": 0.0,
                "secondsSinceLastActive": 0.0,
                "usedInHotkeys": [],
                "parameters": []
            },
            {
                "name": "SignShock",
                "file": "SignShock.exp3.json",
                "active": false,
                "deactivateWhenKeyIsLetGo": false,
                "autoDeactivateAfterSeconds": false,
                "secondsRemaining": 0.0,
                "secondsSinceLastActive": 0.0,
                "usedInHotkeys": [],
                "parameters": []
            }
        ]
        """
    async def Requesting_list_of_available_tracking_parameters(self,websocket,requestID="MyIDWithLessThan64Characters1"):
        #19:使用可能なトラッキングパラメーターのリストを要求する
        """返答例：
        [{'defaultValue': 0.0,
        'max': 1.0,
        'min': 0.0,
        'name': 'HandRightFinger_4_Ring',
        'value': 0.0},
        {'defaultValue': 0.0,
        'max': 1.0,
        'min': 0.0,
        'name': 'HandRightFinger_5_Pinky',
        'value': 0.0}]"""

        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "InputParameterListRequest"
            }
        
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        expression_result = json_response['data']['defaultParameters']
        result = []
        for dictionary in expression_result:
            add_data = {}
            for name, value in dictionary.items():
                if name == "name" or name== "value" or name== "min" or name== "max" or name=="defaultValue":
                    add_data[name]=value
            result.append(add_data)
        return result
        pprint(result)
        print(json.dumps(json_response,indent=4))
    
    async def Get_the_value_for_one_specific_parameter_default_or_custom(self,websocket,ParamName,requestID="MyIDWithLessThan64Characters1"):
        #20:特定の1つのパラメータ（デフォルトまたはカスタム）の値を取得します。
        #配列にて、値を返す
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "ParameterValueRequest",
            "data": {
                "name":ParamName
                }
        }
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        result = json_response['data']
        return result
        pprint(result)
        print(json.dumps(json_response,indent=4))
    
    async def Get_the_value_for_all_Live2D_parameters_in_the_current_model(self,websocket,requestID="MyIDWithLessThan64Characters1"):
        #21:現在のモデルにおけるすべてのLive2Dパラメータの値を取得する
        #配列にて、値を返す
        """return Example
        [
            {'defaultValue': 0.0,
                 'max': 30.0,
                 'min': -30.0,
                 'name': 'ParamAngleX',
                 'value': -1.5333151817321777},
                {'defaultValue': 0.0,
                'max': 30.0,
                'min': -30.0,
                'name': 'ParamAngleY',
                'value': 19.998842239379883}
                ]"""
        #Live2D keyをVTSAPI関数に置き換える辞書関数{Live2Dkey:VTSkey}
        mapping = {'ParamAngleX':'FaceAngleX','ParamAngleY':'FaceAngleY','ParamAngleZ':'FaceAngleZ','ParamEyeBallX':'EyeLeftX','ParamEyeBallY':'EyeLeftY','ParamMouthForm':'MouthSmile','ParamMouthOpenY':'MouthOpen','ParamBrowLY':'BrowLeftY','ParamBrowRY':'BrowRightY','ParamEyeLOpen':'EyeOpenLeft','ParamEyeROpen':'EyeOpenRight'}
        eyeR_mapping={'EyeLeftX':'EyeRightX','EyeLeftY':'EyeRightY'}
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": self.api_ver,
            "requestID": requestID,
            "messageType": "Live2DParameterListRequest"
        }

        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        result = json_response['data']['parameters']

        #VTS API向けに関数名を変換
        result_VTS=[]
        for item in result:
            new_item = item.copy()
            new_item['value'] = round(item['value'],1)

            if new_item['name'] in mapping:
                new_item['name']=mapping[new_item['name']]
            
            result_VTS.append(new_item)
            #右目関数はないので、左目データをコピーする。
            if new_item['name'] in eyeR_mapping:
                eye_R=new_item.copy()
                eye_R['name']=eyeR_mapping[new_item['name']]
                result_VTS.append(eye_R)
        
        return result_VTS
        pprint(result)
        print(json.dumps(json_response,indent=4))
    
    async def Feeding_in_data_for_default_or_custom_parameters(self,websocket,Parameters):
        #24:デフォルトまたはカスタムパラメーターのデータを投入する
        # ParameterValues Example
        """
        {
            "id": "FaceAngleX",
            "value": 12.31
        },
        {
            "id": "MyNewParamName",
            "weight": 0.8,
            "value": 0.7
        }
        """
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": "1.0",
            "requestID": "SomeID",
            "messageType": "InjectParameterDataRequest",
            "data": {
                "faceFound": False,
                "mode": "set",
                "parameterValues": Parameters
            }
        }
        #print(json.dumps(request,indent=4))
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)
        return json_response
        print(json.dumps(json_response,indent=4))

    async def Reset_all_expressions(self,websocket):
        #すべてのexpressionsをリセット
        expressions_list=await self.Requesting_current_expression_state_list(websocket=websocket,requestID="Reset_all_expressions")
        for dictionary in expressions_list:
            json_name = ""
            expression_reset =False
            for name, value in dictionary.items():
                if name == "active" and value == True:
                    expression_reset = True
                elif name == "file":
                    json_name = value
            #表情のリセット実行
            if expression_reset:
                await self.Requesting_activation_or_deactivation_of_expressions(websocket,json_name,active=False,requestID="Reset_expression")

    async def request_token(self,websocket,pluginName,pluginDeveloper,api_ver,request_ID="SomeID"):
        #１回のみ必要な認証処理 返り値は、tokenデータ
        #pngファイル読み込み
        file_path = os.path.join(self.current_directory,"VtuberStudio_plugin","icon.png")
        icon=self.encode_image(file_path)
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": api_ver,
            "requestID": request_ID,
            "messageType": "AuthenticationTokenRequest",
            "data": {
                "pluginName": pluginName,
                "pluginDeveloper": pluginDeveloper,
                "pluginIcon": icon
                }
            }
        print(json.dumps(request))
        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)

        if json_response["messageType"] == "AuthenticationTokenResponse":
            return json_response["data"]["authenticationToken"]
        else:
            print(f"認証エラー: {json_response}")
            return None
    
    async def authenticate(self,websocket, plugin_name, plugin_developer, api_ver, authentication_token,request_ID="SomeID"):
        #トークン認証確認
        request = {
            "apiName": "VTubeStudioPublicAPI",
            "apiVersion": api_ver,
            "requestID": request_ID,
            "messageType": "AuthenticationRequest",
            "data": {
                "pluginName": plugin_name,
                "pluginDeveloper": plugin_developer,
                "authenticationToken": authentication_token
            }
        }

        await websocket.send(json.dumps(request))
        response = await websocket.recv()
        json_response = json.loads(response)

        if json_response["messageType"] == "AuthenticationResponse":
            return json_response["data"]["authenticated"]
        else:
            print(f"認証エラー: {json_response}")
            return None
    
    async def Authentication_Check(self):
        #認証処理
        if self.VTS_token == None:
            #初回認証の実施
            key = None
            async with websockets.connect(self.uri) as websocket:
                key = await self.request_token(websocket=websocket,pluginName=self.pluginName,pluginDeveloper=self.pluginDeveloper,api_ver=self.api_ver)
                if key:
                    #キーの保存
                    with open(self.setting_file_path,"r", encoding='utf-8')as file:
                        lines = file.readlines()
                    updated_lines = []
                    for line in lines:
                        if "token:" in line:
                            updated_lines.append(line + "\n" + key)
                            #updated_lines.append(key)
                        else:
                            updated_lines.append(line)
                    with open(self.setting_file_path,"w", encoding='utf-8')as file:
                        file.writelines(updated_lines)
                    self.VTS_token=key
                    self.key_check = True
                    print("AI_Tuber_APIの認証成功")
                else:
                    raise UserWarning("VTS認証が完了できませんでした。")
        elif self.key_check != True:
            #すでにkeyを取得している+初回の場合　キー有効確認
            async with websockets.connect(self.uri) as websocket:
                result = await self.authenticate(websocket=websocket,plugin_name=self.pluginName,plugin_developer=self.pluginDeveloper,api_ver=self.api_ver,authentication_token=self.VTS_token)
                if result:
                    print("VTSの認証が完了しました。")
                    self.key_check = True
                elif result == False:
                    raise UserWarning("VTS Tokenが正しくありません。再認証する場合は、Setting.txtのTokenの値を消去してください。")
                else:
                    raise UserWarning(f"VTS認証が完了できませんでした。/n{result}")

    async def hotkey_demo(self):
        hotkey_list={}
        try:
            async with websockets.connect(self.uri) as websocket:
                #認証確認
                await self.authenticate(websocket=websocket,plugin_name=self.pluginName,plugin_developer=self.pluginDeveloper,api_ver=self.api_ver,authentication_token=self.VTS_token)
                
                hotkey_list = await self.Requesting_list_of_hotkeys(websocket)
                #pprint(hotkey_list)
                for dictionary in hotkey_list:
                    for name, value in dictionary.items():
                        if name == "name":
                            print(f"{value}を実行/")
                        elif name == "file":
                            expressionFile = value
                            print(expressionFile)
                        elif name == "hotkeyID":
                            if "exp3" in expressionFile:
                                #表情の時、
                                await self.Reset_all_expressions(websocket)#表情のリセット
                                await self.Requesting_execution_of_hotkeys(websocket,hotkeyID=value)
                                await self.Requesting_current_expression_state_list(websocket=websocket) #表情状態の取得
                                time.sleep(3)
                            elif "motion3" in expressionFile:
                                await self.Requesting_execution_of_hotkeys(websocket,hotkeyID=value)
                                time.sleep(3)
        except Exception as e:
            print(f"WebSocket接続時にエラーが発生しました: {e}")
    
    async def get_value_demo(self):
        try:
            async with websockets.connect(self.uri) as websocket:
                #認証確認
                await self.authenticate(websocket=websocket,plugin_name=self.pluginName,plugin_developer=self.pluginDeveloper,api_ver=self.api_ver,authentication_token=self.VTS_token)
                result=await self.Get_the_value_for_all_Live2D_parameters_in_the_current_model(websocket)
                pprint(result)
                print("""
                
                """)
                all_params = await self.Requesting_list_of_available_tracking_parameters(websocket)
                pprint(all_params)
                    
        except Exception as e:
            print(f"WebSocket接続時にエラーが発生しました: {e}")
    
    async def simple_motion_demo(self):
        try:
            async with websockets.connect(self.uri) as websocket:
                #認証確認
                await self.authenticate(websocket=websocket,plugin_name=self.pluginName,plugin_developer=self.pluginDeveloper,api_ver=self.api_ver,authentication_token=self.VTS_token)
                await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":'FaceAngleZ',"value":30.0}])
                    
        except Exception as e:
            print(f"WebSocket接続時にエラーが発生しました: {e}")
    
    async def emotion_demo(self):
        try:
            async with websockets.connect(self.uri) as websocket:
                #認証確認
                await self.authenticate(websocket=websocket,plugin_name=self.pluginName,plugin_developer=self.pluginDeveloper,api_ver=self.api_ver,authentication_token=self.VTS_token)
                #すべてのパラメータの取得
                all_params = await self.Requesting_list_of_available_tracking_parameters(websocket)
                print(all_params)
                duration = 1000 #アニメーション時間(ms)
                """
                for t in range(0, duration + 1,10):#10msごとに更新
                    min=int(all_params[5]['min'])
                    space=int(all_params[5]['max'])-int(all_params[5]['min'])
                    value = self.easeInOutCubic(t,min,space,duration)
                    print(value)
                    await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":all_params[5]['name'],"value":str(value)}])
                    time.sleep(0.01)
                """
                    
                for item in all_params:
                    print(f"Name:{item['name']} //",end="")
                    print(f"Value:[{item['min']}/",end="")
                    await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":item['name'],"value":item['max']}])
                    time.sleep(2)
                    print(f"{item['max']}/",end="")
                    await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":item['name'],"value":item['min']}])
                    time.sleep(2)
                    print(f"{(item['max']-item['min'])/2}/",end="")
                    await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":item['name'],"value":(item['max']-item['min'])/2}])
                    time.sleep(2)
                    print(f"{item['defaultValue']}]")
                    await self.Feeding_in_data_for_default_or_custom_parameters(websocket,[{"id":item['name'],"value":item['defaultValue']}])
                    input("Press Enter")
                #track_params = await self.Get_the_value_for_one_specific_parameter_default_or_custom(websocket,"FacePositionX")
                
        except Exception as e:
            print(f"WebSocket接続時にエラーが発生しました: {e}")

if __name__ == "__main__":         
    #認証確認
    asyncio.get_event_loop().run_until_complete(VTS_main().Authentication_Check())
    asyncio.run(VTS_main().get_value_demo())
    #asyncio.run(VTS_main().simple_motion_demo())
Module/VtubeStudio_API.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
import requests
import os

DEEPL_KEY = os.getenv("DEEPL_KEY")

color_dic = {"black":"\033[30m", "red":"\033[31m", "green":"\033[32m", "yellow":"\033[33m", "blue":"\033[34m", "end":"\033[0m"}
def print_color(text, color="red"):
    print(color_dic[color] + text + color_dic["end"])

def translate_client(text,target_language='JA' ,source_language='EN',API_KEY=DEEPL_KEY):
    params = {
            'auth_key' : API_KEY,
            'text' : text,
            'source_lang' : source_language, # 翻訳対象の言語
            "target_lang": target_language  # 翻訳後の言語
    }
    request = requests.post("https://api-free.deepl.com/v2/translate", data=params) 
    result = request.json()["translations"][0]["text"]
    #print_color(f"{source_language}:{text}\n{target_language}:{result}","blue")
    return result

if __name__ == "__main__": 
    print(translate_client("趣味は何ですか？","EN","JA"))
Module/deepl_translator.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
color_dic = {"black":"\033[30m", "red":"\033[31m", "green":"\033[32m", "yellow":"\033[33m", "blue":"\033[34m", "end":"\033[0m"}
def print_color(text, color="red",end=False):
    if end:
        print(color_dic[color] + text + color_dic["end"],end='')
    else:
        print(color_dic[color] + text + color_dic["end"])
Module/print_color.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
"""
このモジュールは、音声合成のVoiceVox APIと直接通信するためのPythonスクリプトです。
主な機能は、音声のプリセットを作成、更新、削除し、利用可能なスピーカーを取得、そしてテキストを音声に変換することです。さらに、合成された音声を再生するためのオーディオストリームを開始・停止する機能も提供します。

プリセットを新規作成し、音声データを作成、再生するサンプルコード:

Add_preset(id=0,name="ナースロボ-ノーマル",speaker_uuid="882a636f-3bac-431a-966d-c5e6bba9f949",style_id=47,speedScale=1.15,pitchScale=0.0,intonationScale=1.3,volumeScale=1.0,prePhonemeLength=0.0,postPhonemeLength=0.0)
stream = audio_stream_start()
data=text_to_wave('''みなさん、こんにちは！ みらいちゃんことみらいです！''',preset_id=0,speaker=47)
stream.write(data)
print(f"処理時間:{time.time()-processtime}")
audio_stream_stop(stream)
"""

# coding: utf-8
import requests
import json
import time
import pyaudio
import platform
from pprint import pprint

def audio_query(text, speaker, max_retry=3):
    """
    音声合成のための問い合わせを作成する関数。

    Args:
        text (str): 音声合成するテキスト。
        speaker (int): 使用するスピーカーのID。
        max_retry (int, optional): リトライの最大回数。デフォルトは3。

    Raises:
        ConnectionError: リトライ回数が上限に達した場合。

    Returns:
        dict: 問い合わせデータ。
    """
    query_payload = {"text": text, "speaker": speaker}
    for query_i in range(max_retry):
        r = requests.post("http://127.0.0.1:50021/audio_query", 
                        params=query_payload, timeout=(10.0, 300.0))
        if r.status_code == 200:
            query_data = r.json()
            break
        time.sleep(1)
    else:
        raise ConnectionError("リトライ回数が上限に到達しました。 audio_query : ", "/", text[:30], r.text)
    return query_data

def audio_query_from_preset(text, preset_id, max_retry=3):
    """
    プリセットを使用して音声合成のための問い合わせを作成する関数。

    Args:
        text (str): 音声合成するテキスト。
        preset_id (int): 使用するプリセットのID。
        max_retry (int, optional): リトライの最大回数。デフォルトは3。

    Raises:
        ConnectionError: リトライ回数が上限に達した場合。

    Returns:
        dict: 問い合わせデータ。
    """
    query_payload = {"text": text, "preset_id": preset_id}
    for query_i in range(max_retry):
        headers = {'accept': 'application/json'}
        r = requests.post("http://127.0.0.1:50021/audio_query_from_preset", 
                        params=query_payload,headers=headers,timeout=(10.0, 300.0))
        if r.status_code == 200:
            query_data = r.json()
            break
        time.sleep(1)
    else:
        raise ConnectionError("リトライ回数が上限に到達しました。 audio_query : ", "/", text[:30], r.text)
    return query_data

def synthesis(speaker, query_data,max_retry):
    """
    与えられた問い合わせデータを使用して音声合成を行う関数。

    Args:
        speaker (int): 使用するスピーカーのID。
        query_data (dict): 音声合成のための問い合わせデータ。
        max_retry (int): リトライの最大回数。

    Raises:
        ConnectionError: リトライ回数が上限に到達した場合。

    Returns:
        bytes: 音声ファイルのデータ。
    """
    synth_payload = {"speaker": speaker,"enable_interrogative_upspeak": "true"}
    headers = {"accept": "audio/wav", "Content-Type": "application/json"}
    for synth_i in range(max_retry):
        r = requests.post("http://127.0.0.1:50021/synthesis", params=synth_payload, 
                          data=json.dumps(query_data),headers=headers,timeout=(10.0, 300.0))
        if r.status_code == 200:
            #音声ファイルを返す
            return r.content
        time.sleep(1)
    else:
        raise ConnectionError("synthesis エラー：リトライ回数が上限に到達しました。 synthesis : ", r)

def Get_presets():
    """
    すべての作成されたプリセットを取得する関数。

    Returns:
        dict: プリセットのデータ。
    
    Return example:
    [{'id': 0,
  'intonationScale': 1.3,
  'name': 'ナースロボ-ノーマル',
  'pitchScale': 0.0,
  'postPhonemeLength': 0.0,
  'prePhonemeLength': 0.0,
  'speaker_uuid': '882a636f-3bac-431a-966d-c5e6bba9f949',
  'speedScale': 1.15,
  'style_id': 47,
  'volumeScale': 1.0}]
    """
    r = requests.get("http://127.0.0.1:50021/presets")
    if r.status_code == 200:
        query_data = r.json()
        return query_data
    else:
        print(f"Error: Received status code {r.status_code}")
        return None

def Add_preset(id: int,name: str,speaker_uuid: str,style_id=0,speedScale=1.0,pitchScale=0,intonationScale=1.0,volumeScale=1.00,prePhonemeLength=0,postPhonemeLength=0,max_retry=3):
    """
    新しいプリセットを追加する関数。
    Args:
        id (int): プリセットのID。任意番号でよい。このIDをプリセットを呼び出す際用いる。
        name (str): プリセットの名前。任意の名前で良い。
        speaker_uuid (str): 使用するスピーカーのUUID。
        style_id (int, optional): スタイルID。デフォルトは0。
        speedScale (float, optional): 速度のスケール。デフォルトは1.0。
        pitchScale (float, optional): ピッチのスケール。デフォルトは0。
        intonationScale (float, optional): 抑揚のスケール。デフォルトは1.0。
        volumeScale (float, optional): 音量のスケール。デフォルトは1.0。
        prePhonemeLength (float, optional): 開始無音の長さ。デフォルトは0。
        postPhonemeLength (float, optional): 終了無音の長さ。デフォルトは0。
        max_retry (int, optional): リトライの最大回数。デフォルトは3。

    Raises:
        ConnectionError: リトライ回数が上限に到達した場合。

    Returns:
        dict: 問い合わせデータ。
    
    Example:
        Add_preset(id=0,name="ナースロボ-ノーマル",speaker_uuid="882a636f-3bac-431a-966d-c5e6bba9f949",style_id=47,speedScale=1.15,pitchScale=0.0,intonationScale=1.3,volumeScale=1.0,prePhonemeLength=0.0,postPhonemeLength=0.0)
    """
    preset_payload = {
    "id": id,
    "name": name,
    "speaker_uuid": speaker_uuid,   #speakersより取得すること
    "style_id": style_id,           #speakersより取得すること
    "speedScale": speedScale,       #話速
    "pitchScale": pitchScale,       #音高
    "intonationScale": intonationScale, #抑揚
    "volumeScale": volumeScale,         #音量
    "prePhonemeLength": prePhonemeLength,   #開始無音
    "postPhonemeLength": postPhonemeLength  #終了無音
    }
    for query_i in range(max_retry):
        r = requests.post("http://127.0.0.1:50021/add_preset", 
                        json=preset_payload, timeout=(10.0, 300.0))
        if r.status_code == 200:
            query_data = r.json()
            break
        time.sleep(1)
        print("Add_preset 再問い合わせ")
    else:
        raise ConnectionError("Add_preset エラー：リトライ回数が上限に到達しました。 synthesis : ", r.json())

def update_preset(id: int,name: str,speaker_uuid: str,style_id=0,speedScale=1.0,pitchScale=0,intonationScale=1.0,volumeScale=1.00,prePhonemeLength=0,postPhonemeLength=0,max_retry=3):
    """
    既存のプリセットを更新する関数。

    Args:
        同上のAdd_preset関数参照。

    Raises:
        ConnectionError: リトライ回数が上限に到達した場合。

    Returns:
        dict: 問い合わせデータ。
    """

    preset_payload = {
    "id": id,
    "name": name,
    "speaker_uuid": speaker_uuid,   #speakersより取得すること
    "style_id": style_id,           #speakersより取得すること
    "speedScale": speedScale,       #話速
    "pitchScale": pitchScale,       #音高
    "intonationScale": intonationScale, #抑揚
    "volumeScale": volumeScale,         #音量
    "prePhonemeLength": prePhonemeLength,   #開始無音
    "postPhonemeLength": postPhonemeLength  #終了無音
    }
    for query_i in range(max_retry):
        r = requests.post("http://127.0.0.1:50021/update_preset", 
                        json=preset_payload, timeout=(10.0, 300.0))
        if r.status_code == 200:
            query_data = r.json()
            break
        time.sleep(1)
    else:
        raise ConnectionError("音声エラー：リトライ回数が上限に到達しました。 synthesis : ", r.json())

def delete_preset(id:int):
    """
    プリセットを削除する関数。

    Args:
        id (int): 削除するプリセットのID。

    Returns:
        bool: プリセットが正常に削除された場合はTrue、それ以外の場合はFalse。
    """
    payload = {'id': id}
    r = requests.post("http://127.0.0.1:50021/delete_preset", params=payload)
    if r.status_code == 204:
        print(f"Preset successfully deleted at ID{id}.")
        return True
    else:
        print(f"delete_preset Error: Received status code {r.status_code} at delete id {id}")
        return False

def remove_all_presets():
    presetlist=Get_presets()
    #delete all preset
    if presetlist != []:
        for item in presetlist:
            delete_preset(id=item['id'])

def speakers():
    """
    利用可能なすべてのspeakerを取得する関数。stylesやspeaker_uuidを取得する関数。

    Returns:
        dict: スピーカーのデータ、またはエラーが発生した場合はNone。

    Return Example:
    [{'name': '四国めたん',
  'speaker_uuid': '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff',
  'styles': [{'id': 2, 'name': 'ノーマル'},
             {'id': 0, 'name': 'あまあま'},
             {'id': 6, 'name': 'ツンツン'},
             {'id': 4, 'name': 'セクシー'},
             {'id': 36, 'name': 'ささやき'},
             {'id': 37, 'name': 'ヒソヒソ'}],
  'supported_features': {'permitted_synthesis_morphing': 'SELF_ONLY'},
  'version': '0.14.3'},
 {'name': 'ずんだもん',
  'speaker_uuid': '388f246b-8c41-4ac1-8e2d-5d79f3ff56d9',
  'styles': [{'id': 3, 'name': 'ノーマル'},
             {'id': 1, 'name': 'あまあま'},
             {'id': 7, 'name': 'ツンツン'},
             {'id': 5, 'name': 'セクシー'},
             {'id': 22, 'name': 'ささやき'},
             {'id': 38, 'name': 'ヒソヒソ'}],
  'supported_features': {'permitted_synthesis_morphing': 'SELF_ONLY'},
  'version': '0.14.3'}]
    """
    r = requests.get("http://127.0.0.1:50021/speakers")
    if r.status_code == 200:
        query_data = r.json()
        pprint(query_data)
        return query_data
    else:
        print(f"Error: Received status code {r.status_code}")
        return None
    
#Process4
def audio_stream_start():
    """
    オーディオストリームを開始する関数。

    Returns:
        object: 開始されたストリーム、またはVB-Audio Virtualが見つからない場合はNone。
    """

    p = pyaudio.PyAudio()

    # VB-CABLEのインデックスを見つける
    current_os = platform.system()
    mic_name =""
    vb_cable_index = -1
    if current_os == "Windows":
        mic_name = "CABLE Input"
    else:
        mic_name = "VB"

    for i in range(p.get_device_count()):
        dev_info = p.get_device_info_by_index(i)
        #print(dev_info['name'])
        if mic_name in dev_info["name"]:
            vb_cable_index = dev_info["index"]
            #print(f'再生デバイス名: {dev_info["name"]}')
            break

    if vb_cable_index == -1:
        print("VB-Audio Virtualが見つかりませんでした。インストール済みかどうか確認してください。")
        return
    
    # ストリームを開く
    stream = p.open(format=pyaudio.paInt16,
                    channels=1,
                    rate=24000,
                    output=True,
                    output_device_index=vb_cable_index)
    #プツ音防止の為、待機
    time.sleep(0.4)
    return stream
#Process4
def audio_stream_stop(stream):
    """
    オーディオストリームを停止する関数。

    Args:
        stream (object): 停止するストリーム。
    """
    p = pyaudio.PyAudio()
    stream.stop_stream()
    stream.close()

    p.terminate()

def text_to_wave(texts, preset_id=None, speaker=8, max_retry=20):
    """
    テキストを音声に変換する関数。

    Args:
        texts (str): 音声に変換するテキスト。
        preset_id (int, optional): 使用するプリセットのID。
        speaker (int, optional): 使用するスピーカーのID。デフォルトは8。
        max_retry (int, optional): リトライの最大回数。デフォルトは20。

    Returns:
        bytes: 音声ファイルのデータ。
    """
    #http://localhost:50021/speakers
    if texts == "":
        return
    
    # audio_query
    if preset_id is not None:
        query_data = audio_query_from_preset(text=texts,preset_id=preset_id)
    else:
        query_data = audio_query(texts,speaker,max_retry)
    # synthesis
    voice_data=synthesis(speaker,query_data,max_retry)
    return voice_data

if __name__ == "__main__":
    #Add_preset(id=0,name="ナースロボ-ノーマル",speaker_uuid="882a636f-3bac-431a-966d-c5e6bba9f949",style_id=47,speedScale=1.37,pitchScale=0,intonationScale=1,volumeScale=1,prePhonemeLength=0,postPhonemeLength=0)
    presetlist=Get_presets()
    pprint(presetlist)
    #delete all preset
    '''
    if presetlist != []:
        for item in presetlist:
            delete_preset(id=item['id'])
            '''

    #プリセット追加
    Add_preset(id=1,name="ナースロボ-ノーマル",speaker_uuid="882a636f-3bac-431a-966d-c5e6bba9f949",style_id=47,speedScale=1.15,pitchScale=0.0,intonationScale=1.3,volumeScale=1.0,prePhonemeLength=0.0,postPhonemeLength=0.0)
    #Add_preset(id=1,name="ナースロボ-恐怖",speaker_uuid="882a636f-3bac-431a-966d-c5e6bba9f949",style_id=49,speedScale=1,pitchScale=0,intonationScale=1,volumeScale=1,prePhonemeLength=0,postPhonemeLength=0)

    processtime=time.time()
    stream = audio_stream_start()
    data=text_to_wave("""みなさん、こんにちは！ みらいちゃんことみらいです！""",preset_id=0,speaker=47)
    stream.write(data)
    data=text_to_wave("""みなさん、こんにちは！ みらいちゃんことみらいです！""",preset_id=1,speaker=47)
    stream.write(data)
    print(f"処理時間:{time.time()-processtime}")
    audio_stream_stop(stream)
Module/voicevox.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0

import os
import openai
import time
import re
import json
import websocket
import Module.VtubeStudio_API as VtubeStudio_API
import asyncio
from Module.voicevox import Add_preset,update_preset, audio_stream_start, text_to_wave, audio_stream_stop , Get_presets, remove_all_presets
from pprint import pprint
from Module.print_color import print_color

def VoiceVoxGPT_AutoPich(OpenAI_key,emotion,AI_model = 'gpt-3.5-turbo',max_retries=3):
    """
    この関数は、OpenAIのGPT-3.5モデルを利用して、指定した感情に基づくLive2Dパラメータを生成します。
    生成したパラメータはVtubeStudio_emotionに提供することで、Live2Dモデルの動きに変換することができます。
    この関数の応答には10秒から30秒ほど時間がかかります。キャラクターをアニメーションさせる場合は、先にすべてのモーションを保存しておくことをお勧めします。

    引数:
    OpenAI_key -- OpenAIのAPIキー [String]
    emotion -- 表現したい感情 [String]
    AI_model -- OpenAI社が提供しているモデルを設定 [String]

    戻り値:
    dictionary -- VtubeStudio_APIに渡すためのLive2Dパラメータの辞書を返します。Live2Dのパラメータ辞書はVtubeStudio_emotionのemotion_listに用いることができます。取得出来なかった場合はFalseを返します。 [Dictionary]
    """
    openai.api_key=OpenAI_key
    Prompt_template = """I would like you to estimate the vocal quality of a character's speech based on their emotions.
## Rules
Estimate the character's 'speakingSpeed','pitch', and 'intonation' from their emotions.
The 'speakingSpeed' should be selected from 'fast, 'medium', 'slow',
The 'pitch' should be selected from 'high', 'normal', 'low',
The 'intonation' should be selected from 'high', 'normal', 'low'.
These values need to be adjusted based on the content and description.

##Example
Input: excited about Mario Kart and teasingly criticizes someone's bad skills.
Output: {"speakingSpeed":"fast","pitch":"high","intonation":"high"}"""

    Prompt = Prompt_template
    result = ""
    #print_color(f"Prompt:{Prompt}","green")

    #GPT3.5にて問い合わせ
    #print_color(f'{AI_model} Stream ViceVox問い合わせ開始 ','yellow',True)
    GPT_time= time.time()

    #システムプロンプトの追加
    ChatGPT_Prompt=[{"role": "system", "content": Prompt}]
    #ユーザープロンプトの追加
    ChatGPT_Prompt.append({"role": "user", "content": emotion})
    for i in range(max_retries):
        try:
            for chunk in openai.ChatCompletion.create(
                model=AI_model,
                messages=ChatGPT_Prompt,
                stream=True,
            ):
                content = chunk["choices"][0].get("delta", {}).get("content")
                #print_color(f"{content}",'green',True)
                if content is not None:
                    if '}' in content:
                        result +=content
                        break
                    result += content
            # 終了したらループを抜ける
            break
        except openai.error.RateLimitError:
            # エラーメッセージを表示
            print_color(f"モデルが他のリクエストで過負荷となっています。")
            # エラーが持続する場合はOpenAIのヘルプセンターに連絡を促すメッセージを表示
            print_color(f"エラーが持続する場合は、help.openai.com に連絡してください。")
            # リトライの間隔を設定（ここでは例として1秒）
            time.sleep(1)
        except Exception as e:
            # その他のエラーを表示
            print_color(f"OpenAI問い合わせ中に、予期せぬエラーが発生しました：{e}")
            break
    
    #GPTの結果を取得
    print_color(f"[自動声質]{AI_model} 問い合わせ時間: {(time.time()-GPT_time):.1f}sec","yellow")
    #print(result)
    #Json部分のみを切り取りして、辞書配列に変換
    match = re.search(r'\{(.|\n)+\}', result)
    if match is not None:
        json_string = match.group()
        try:
            dictionary = json.loads(json_string)
        except json.JSONDecodeError as e:
            print(f"[自動声質] JSON decode error: {e}\nResult: {result}")
            # ここでエラー処理を行うか、単にパスして次の処理に進みます。
            pass
    else:
        # 一致が見つからなかった場合のエラー処理を行う
        print_color("エラー: モーションについて問い合わせを行ったがモーションが正しく取得できなかった。")
        print_color(f"{result}")
        return False
    return dictionary

def Create_AutoPich_Preset(characterPreset,voice_data,presetid=0):
    '''Example characterPreset = {
        'CharacterName': 'みらい',
        'speaker_uuid': '882a636f-3bac-431a-966d-c5e6bba9f949',
        'style_id':47,
        'speakingSpeed':{'min':0.85,'default':1.0,'max':1.2},
        'pitch':{'min':-0.05,'default':0.0,'max':0.04},
        'intonation':{'min':0.6,'default':1.0,'max':1.4}
        }

        voice_data = {"speakingSpeed":"fast","pitch":"high","intonation":"high"}'''
    
    for key,value in voice_data.items():
        if value == 'fast' or value == 'high':
            voice_data[key] = characterPreset[key]["max"]
        elif value == "medium" or value == 'normal':
            voice_data[key] = characterPreset[key]["default"]
        elif value == "low" or value == 'slow':
            voice_data[key] = characterPreset[key]["min"]
        else:
            voice_data[key] = characterPreset[key]["default"]
    
    #Check Preset
    allpresets=Get_presets()
    #設定プリセットがある場合
    if any(d['id'] == presetid for d in allpresets):
        update_preset(presetid,'mirai_preset',speaker_uuid=characterPreset['speaker_uuid'], style_id=characterPreset['style_id'],speedScale=voice_data['speakingSpeed'],pitchScale=voice_data['pitch'],intonationScale=voice_data['intonation'])
    else:
        Add_preset(presetid,'mirai_preset',speaker_uuid=characterPreset['speaker_uuid'], style_id=characterPreset['style_id'],speedScale=voice_data['speakingSpeed'],pitchScale=voice_data['pitch'],intonationScale=voice_data['intonation'])

if __name__ == "__main__":
    
    # OpenAIのAPIキーを設定します
    OpenAI_key = os.getenv("OPENAI_API_KEY")
    emotion = 'Excited amusement for Mario Kart, playful teasing, and positive encouragement.'
    characterPreset = {
        'CharacterName': 'みらい',
        'speaker_uuid': '882a636f-3bac-431a-966d-c5e6bba9f949',
        'style_id':47,
        'speakingSpeed':{'min':0.85,'default':1.0,'max':1.2},
        'pitch':{'min':-0.05,'default':0.0,'max':0.04},
        'intonation':{'min':0.6,'default':1.0,'max':1.4}
        }
    #result = asyncio.run( VoiceVoxGPT_AutoPich(OpenAI_key,emotion))
    result = VoiceVoxGPT_AutoPich(OpenAI_key,emotion)
    print_color(f'{result}')
    Create_AutoPich_Preset(characterPreset,result)
    text_to_wave("音声テスト",0,speaker=47)
Module/voicevox_GPT.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
"""
このモジュールは、OpenAI GPT-3.5の出力とVtubeStudio APIを統合して、
Live2Dモデルの動作を制御するためのものです。特定の感情に基づいて
Live2Dの表情を変化させます。

サンプルコード：
    #VtubeStudio のkey認証
    vtsapi=VtubeStudio_API.VTS_main()
    asyncio.get_event_loop().run_until_complete(vtsapi.Authentication_Check())
    # 表現したい感情のリストを設定します
    emotions = ['happy', 'sad']

    async def animate_model():
        emotion_list = []
        for emotion in emotions:
            # それぞれの感情に対するLive2Dパラメータを生成します。
            emotion_dict = VtubeStudio_motion(OpenAI_key, emotion)
            if emotion_dict:
                emotion_list.append(emotion_dict)
        
        for emotion_data in emotion_list:
            # 感情リストに基づいてLive2Dモデルのアニメーションを行います。
            pprint(emotion_data)
            animation_result = await VtubeStudio_emotion(emotion_data)
            if animation_result:
                print("Animation successful.")
            else:
                print("Animation failed.")
        else:
            print("No emotions could be generated.")

    # 非同期ループを開始し、モデルをアニメーション化します。
    loop = asyncio.get_event_loop()
    loop.run_until_complete(animate_model())
"""

import websockets
import openai
import time
import re
import json
import Module.VtubeStudio_API as VtubeStudio_API
import asyncio
import os
from pprint import pprint
from Module.print_color import print_color

def VtubeStudio_GPTmotion(OpenAI_key,emotion,AI_model = 'gpt-3.5-turbo'):
    """
    この関数は、OpenAIのGPT-3.5モデルを利用して、指定した感情に基づくLive2Dパラメータを生成します。
    生成したパラメータはVtubeStudio_emotionに提供することで、Live2Dモデルの動きに変換することができます。
    この関数の応答には10秒から30秒ほど時間がかかります。キャラクターをアニメーションさせる場合は、先にすべてのモーションを保存しておくことをお勧めします。

    引数:
    OpenAI_key -- OpenAIのAPIキー [String]
    emotion -- 表現したい感情 [String]
    AI_model -- OpenAI社が提供しているモデルを設定 [String]

    戻り値:
    dictionary -- VtubeStudio_APIに渡すためのLive2Dパラメータの辞書を返します。Live2Dのパラメータ辞書はVtubeStudio_emotionのemotion_listに用いることができます。取得出来なかった場合はFalseを返します。 [Dictionary]
    """
    openai.api_key=OpenAI_key
    Prompt_template = """Set Live2D model parameters based on the "{0}" emotion. 

1. Summarize "{0}" face and body posture in two sentences.
2. Convert that into Live2D JSON parameters:

- Face Left/Right Rotation (-30 right, 30 left)
- Face Up/Down Rotation (-30 down, 30 up)
- Face Lean Rotation (-30 head right, 30 head left)
- Eye X (-1 pupils right, 1 pupils left, default 0)
- Eye Y (-1 pupils down, 1 pupils up, default 0)
- Mouth Smile (-1 O-shaped, 1 open with gums, default 0)
- Mouth Open (0 closed, 1 open)
- Brow Height Left (-1 near eye (worried), 1 away eye (surprised), default 0.5)
- Brow Height Right (-1 near eye (worried), 1 away eye (surprised), default 0.5)

Provide a JSON object like:
{{
  "Face Left/Right Rotation": 0,
  "Face Up/Down Rotation": 10,
  "Face Lean Rotation": -5,
  "Eye X": 0.5,
  "Eye Y": 0.2,
  "Mouth Smile": 1,
  "Mouth Open": 0.5,
  "Brow Height Left": 0.5,
  "Brow Height Right": 0.5
}}"""

    Prompt = Prompt_template.format(emotion)
    #print_color(f"Prompt:{Prompt}","green")
    #GPT3.5にて問い合わせ
    print_color(f'{AI_model} 問い合わせ開始 ','yellow',True)
    GPT_time= time.time()
    for _ in range(3): #最大3回リトライ
        try:
            responce=openai.ChatCompletion.create(
                model=AI_model,
                messages=[
                    {"role": "system", "content": Prompt}
                ]
            )
            full_result = responce
            result= responce["choices"][0]["message"]["content"]
        except openai.error.RateLimitError:
            print_color("OpenAI RateLimitError:そのモデルは現在、他のリクエストで過負荷になっています。2秒後に再問い合わせを行います。")
            time.sleep(2)
        except openai.error.OpenAIError as e: # OpenAIErrorを追加して、特定のエラーをキャッチ
            if e.type == "server_error":
                print_color("OpenAI Server Error:あなたのリクエストを処理する際に、サーバーにエラーが発生しました。申し訳ありません！エラーが解決しない場合は、リクエストを再試行するか、ヘルプセンター（help.openai.com）にてお問い合わせください。5秒後に再試行します。")
                time.sleep(5)
            else:
                print_color(f"OpenAI 予期せぬエラー: {e}")
                return False
        except Exception as e:
            print_color(f"OpenAI 予期せぬエラー: {e}")
            return False
    #GPTの結果を取得
    print_color(f"表情変更:{emotion} / {AI_model} 問い合わせ時間: {(time.time()-GPT_time):.1f}sec","yellow")
    
    #print(result)
    #Json部分のみを切り取りして、辞書配列に変換
    match = re.search(r'\{(.|\n)+\}', result)
    if match is not None:
        json_string = match.group()
        dictionary = json.loads(json_string)
    else:
        # 一致が見つからなかった場合のエラー処理を行う
        print_color("エラー: モーションについて問い合わせを行ったがモーションが正しく取得できなかった。")
        print_color(f"{responce}")
        return False
    #print(f"表情{emotion}:{result}")
    #辞書配列名をVTSAPIに適合したものに変更
    #{'Face Left/Right Rotation': 0, 'Face Up/Down Rotation': 10, 'Face Lean Rotation': 0, 'Eye X': 0, 'Eye Y': 0, 'Mouth Smile': 1, 'Mouth Open': 0, 'Brow Height': 0.5, 'Brow Height Right': 0.5}
    dictionary['FaceAngleX']=dictionary.pop('Face Left/Right Rotation')
    dictionary['FaceAngleY']=dictionary.pop('Face Up/Down Rotation')
    dictionary['FaceAngleZ']=dictionary.pop('Face Lean Rotation')
    dictionary['EyeLeftX']=dictionary.pop('Eye X')
    dictionary['EyeLeftY']=dictionary.pop('Eye Y')
    dictionary['EyeRightX']=dictionary['EyeLeftX']
    dictionary['EyeRightY']=dictionary['EyeLeftY']
    dictionary['MouthSmile']=dictionary.pop('Mouth Smile')
    dictionary['MouthOpen']=dictionary.pop('Mouth Open')
    dictionary['BrowLeftY']=dictionary.pop('Brow Height Left')
    dictionary['BrowRightY']=dictionary.pop('Brow Height Right')

    #print('GPTで生成された表情パラメータ')
    return dictionary


def VtubeStudio_GPTmotion_Stream(OpenAI_key,emotion,AI_model = 'gpt-3.5-turbo',max_retries=3):
    """
    この関数は、OpenAIのGPT-3.5モデルを利用して、指定した感情に基づくLive2Dパラメータを生成します。
    生成したパラメータはVtubeStudio_emotionに提供することで、Live2Dモデルの動きに変換することができます。
    この関数の応答には10秒から30秒ほど時間がかかります。キャラクターをアニメーションさせる場合は、先にすべてのモーションを保存しておくことをお勧めします。

    引数:
    OpenAI_key -- OpenAIのAPIキー [String]
    emotion -- 表現したい感情 [String]
    AI_model -- OpenAI社が提供しているモデルを設定 [String]

    戻り値:
    dictionary -- VtubeStudio_APIに渡すためのLive2Dパラメータの辞書を返します。Live2Dのパラメータ辞書はVtubeStudio_emotionのemotion_listに用いることができます。取得出来なかった場合はFalseを返します。 [Dictionary]
    """
    openai.api_key=OpenAI_key
    Prompt_template = """Set Live2D model parameters based on the "{0}" emotion. 

1. Summarize "{0}" face and body posture in two sentences.
2. Convert that into Live2D JSON parameters:

- Face Left/Right Rotation (-30 right, 30 left)
- Face Up/Down Rotation (-30 down, 30 up)
- Face Lean Rotation (-30 head right, 30 head left)
- Eye X (-1 pupils right, 1 pupils left, default 0)
- Eye Y (-1 pupils down, 1 pupils up, default 0)
- Mouth Smile (-1 O-shaped, 1 open with gums, default 0)
- Mouth Open (0 closed, 1 open)
- Brow Height Left (-1 near eye (worried), 1 away eye (surprised), default 0.5)
- Brow Height Right (-1 near eye (worried), 1 away eye (surprised), default 0.5)

Provide a JSON object like:
{{
  "Face Left/Right Rotation": 0,
  "Face Up/Down Rotation": 10,
  "Face Lean Rotation": -5,
  "Eye X": 0.5,
  "Eye Y": 0.2,
  "Mouth Smile": 1,
  "Mouth Open": 0.5,
  "Brow Height Left": 0.5,
  "Brow Height Right": 0.5
}}"""

    Prompt = Prompt_template.format(emotion)
    result = ""
    #print_color(f"Prompt:{Prompt}","green")

    #GPT3.5にて問い合わせ
    #print_color(f'{AI_model} Stream VTS問い合わせ開始 ','yellow',True)
    GPT_time= time.time()

    #システムプロンプトの追加
    ChatGPT_Prompt=[{"role": "system", "content": Prompt}]

    for i in range(max_retries):
        try:
            for chunk in openai.ChatCompletion.create(
                model=AI_model,
                messages=ChatGPT_Prompt,
                stream=True,
            ):
                content = chunk["choices"][0].get("delta", {}).get("content")
                #print_color(f"{content}",'green',True)
                if content is not None:
                    result += content
            # 成功したらループを抜ける
            break
        except openai.error.RateLimitError:
            # エラーメッセージを表示
            print_color(f"モデルが他のリクエストで過負荷となっています。")
            # エラーが持続する場合はOpenAIのヘルプセンターに連絡を促すメッセージを表示
            print_color(f"エラーが持続する場合は、help.openai.com に連絡してください。")
            # リトライの間隔を設定（ここでは例として1秒）
            time.sleep(1)
        except Exception as e:
            # その他のエラーを表示
            print_color(f"OpenAI問い合わせ中に、予期せぬエラーが発生しました：{e}")
            break
    
    #GPTの結果を取得
    print_color(f"[表情変更]{AI_model} 問い合わせ時間: {(time.time()-GPT_time):.1f}sec","yellow")
    
    #print(result)
    #Json部分のみを切り取りして、辞書配列に変換
    match = re.search(r'\{(.|\n)+\}', result)
    if match is not None:
        json_string = match.group()
        dictionary = json.loads(json_string)
    else:
        # 一致が見つからなかった場合のエラー処理を行う
        print_color("エラー: モーションについて問い合わせを行ったがモーションが正しく取得できなかった。")
        print_color(f"{result}")
        return False
    #print(f"表情{emotion}:{result}")
    #辞書配列名をVTSAPIに適合したものに変更
    #{'Face Left/Right Rotation': 0, 'Face Up/Down Rotation': 10, 'Face Lean Rotation': 0, 'Eye X': 0, 'Eye Y': 0, 'Mouth Smile': 1, 'Mouth Open': 0, 'Brow Height': 0.5, 'Brow Height Right': 0.5}
    dictionary['FaceAngleX']=dictionary.pop('Face Left/Right Rotation')
    dictionary['FaceAngleY']=dictionary.pop('Face Up/Down Rotation')
    dictionary['FaceAngleZ']=dictionary.pop('Face Lean Rotation')
    dictionary['EyeLeftX']=dictionary.pop('Eye X')
    dictionary['EyeLeftY']=dictionary.pop('Eye Y')
    dictionary['EyeRightX']=dictionary['EyeLeftX']
    dictionary['EyeRightY']=dictionary['EyeLeftY']
    dictionary['MouthSmile']=dictionary.pop('Mouth Smile')
    dictionary['MouthOpen']=dictionary.pop('Mouth Open')
    dictionary['BrowLeftY']=dictionary.pop('Brow Height Left')
    dictionary['BrowRightY']=dictionary.pop('Brow Height Right')

    #print('GPTで生成された表情パラメータ')
    return dictionary

async def VtubeStudio_emotion(emotion_list,animation_timems=1000):
    """
    この関数は、送られたemotion_listに基づいてLive2Dモデルアニメーションを行います。
    emotion_listから取得した各感情に対応するLive2Dパラメータを利用し、VtubeStudio_APIを
    通じてモデルに適用します。

    引数:
    emotion_list -- Live2Dパラメータを変更する感情のリスト [Dictionary]
    animation_timems -- アニメーションの期間(ミリ秒) [int]

    戻り値:
    True/False -- アニメーションの実行が成功したかどうか [bool]

    注意:
    VtubeStudio_emotionは、async defで宣言されています。
    """

    vts_instance = VtubeStudio_API.VTS_main()
    try:
        async with websockets.connect(vts_instance.uri) as websocket:
            #認証確認
            await vts_instance.authenticate(websocket=websocket,plugin_name=vts_instance.pluginName,plugin_developer=vts_instance.pluginDeveloper,api_ver=vts_instance.api_ver,authentication_token=vts_instance.VTS_token)
            #すべてのパラメータの取得
            all_params = await vts_instance.Get_the_value_for_all_Live2D_parameters_in_the_current_model(websocket)
            #print("取得した現在の状態:")
            #for param in all_params:
                #print(f"{param['name']}/value: {param['value']}")
            #print()
            animation_list=[]

            #VTS APIに問い合わせるデータをまとめた辞書配列を作成する
            #トラッキングパラメータリストより、現在値を、エモーションリストよりターゲット値を取得する
            for param in all_params:
                if param['name'] in list(emotion_list.keys()):
                    if float(param['value'])!=float(emotion_list[param['name']]): #値に変化がない場合は無視する
                        animation_list.append({'name':param['name'],'start_value':param['value'],'target_value':emotion_list[param['name']]})
           # print("アニメーションリクエストする内容:")
            #pprint(animation_list)
            
            #アニメーションの実施
            duration = animation_timems #アニメーション時間(ms)
            for t in range(0, duration + 1,10):#10msごとに更新
                request=[]
                for param in animation_list:
                    request.append({"id":param['name'],"value":vts_instance.easeInOutCubic(t,param['start_value'],round(float(param['target_value'])-float(param['start_value']),1),duration)})
                #print(f"リクエスト内容: {request}")
                
                await vts_instance.Feeding_in_data_for_default_or_custom_parameters(websocket,request)
            return True
            
    except Exception as e:
        print(f"VtubeStudio_emotion Error: WebSocket接続時にエラーが発生しました: {e}")
        return False
    
if __name__ == "__main__":

    # OpenAIのAPIキーを設定します
    OpenAI_key = os.getenv("OPENAI_API_KEY")

    #VtubeStudio のkey認証
    vtsapi=VtubeStudio_API.VTS_main()
    asyncio.get_event_loop().run_until_complete(vtsapi.Authentication_Check())
    # 表現したい感情のリストを設定します
    emotions = ['happy', 'sad']

    async def animate_model():
        emotion_list = []
        for emotion in emotions:
            # それぞれの感情に対するLive2Dパラメータを生成します。
            emotion_dict = VtubeStudio_GPTmotion_Stream(OpenAI_key, emotion)
            if emotion_dict:
                emotion_list.append(emotion_dict)
        
        for emotion_data in emotion_list:
            # 感情リストに基づいてLive2Dモデルのアニメーションを行います。
            # pprint(emotion_data)
            animation_result = await VtubeStudio_emotion(emotion_data)
            if animation_result:
                print("Animation successful.")
            else:
                print("Animation failed.")
        else:
            print("No emotions could be generated.")

    # 非同期ループを開始し、モデルをアニメーション化します。
    loop = asyncio.get_event_loop()
    loop.run_until_complete(animate_model())
Module/vtubeStudio_GPT_motion.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0
import os
import sys
import multiprocessing
import azure.cognitiveservices.speech as speechsdk
import ctypes
import time
import openai
import json
import asyncio
import pyaudio
import platform
from pynput import keyboard
import Module.VtubeStudio_API as VtubeStudio_API
from Module.OBS_plugin_5 import obs_socket
from Module.print_color import print_color
from Module.voicevox import text_to_wave
from Module.deepl_translator import translate_client
from Module.vtubeStudio_GPT_motion import VtubeStudio_GPTmotion_Stream,VtubeStudio_emotion
from Module.voicevox_GPT import VoiceVoxGPT_AutoPich,Create_AutoPich_Preset
 
# 初期設定
speech_key = os.getenv("AZURE_API_KEY")
openai_key = os.getenv("OPENAI_API_KEY")
speech_region = "japaneast"
obs= obs_socket()
scene_name = "auto"
 
# Process1 / 音声認識を行う関数
def recognize_speech(speech_key, speech_region, SubList, stop_event):
    try:
        speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=speech_region, speech_recognition_language='ja-JP')
        audio_config = speechsdk.audio.AudioConfig(use_default_microphone=True)
        speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, audio_config=audio_config)
    except Exception as e:
        print_color(f"エラーが発生しました: {e}")

    print_color("音声認識を開始しました。","green")

    def on_recognized(args):
        recognized_text = args.result.text
        print_color(f"{recognized_text}","blue")
        if recognized_text != "": 
            #print_color(f"{recognized_text}","blue")
            SubList['userSub']+=recognized_text
            obs.text_change(sceneName=scene_name,inputName="subs1",inputSettings={"text":recognized_text},overlay=False,subDisplayAnchor='top',subDisplayOffset=36,fontsize=48,bk_color=8721863,bk_opacity=60)

            DeepL_time = time.time() 
            en_user=translate_client(recognized_text,'EN','JA').replace('\n', '')
            transrate_time = time.time()-DeepL_time
            if transrate_time < 2:
                time.sleep(2-transrate_time)
            else:
                print_color(f'\n<注意>翻訳時間が長い可能性があります。\nDeepLTranslate Time:{time.time()-DeepL_time}sec\n{en_user}',"yellow")
            
            obs.text_change(sceneName=scene_name,inputName="subs1",inputSettings={"text":en_user},overlay=False,subDisplayAnchor='top',subDisplayOffset=36,fontsize=48,bk_color=8721863,bk_opacity=60)
    def on_canceled(args):
        if args.reason == speechsdk.CancellationReason.Error:
            print_color(f"音声認識エラーが発生しました: {args.error_details}")

    try:
        speech_recognizer.recognized.connect(on_recognized)
        speech_recognizer.canceled.connect(on_canceled)  # エラーハンドリングを追加
        speech_recognizer.start_continuous_recognition()
    except Exception as e:
        print(f"エラーが発生しました: {e}")
    # stop_eventがセットされるまで音声認識を続ける
    stop_event.wait()
    speech_recognizer.stop_continuous_recognition()

# Process2 / GPTに問い合わせを行うプロセス
def gpt_process(SubList,gpt_result_list,openai_key,key_flag,end_flag,MemoryList):
    Talk_Data_str=""
    with end_flag.get_lock():
        loop_flag = end_flag.value
    
    while not loop_flag:
        read_flag = False
        with key_flag.get_lock():
            if key_flag.value:
                read_flag = True
                key_flag.value = False
        if read_flag:
            #読み上げフラグが立っている
            if not SubList['userSub']=="":
                Talk_Data_str = SubList['userSub']
                OpenAI_Stream(openai_key,Talk_Data_str,gpt_result_list,MemoryList)
                Talk_Data_str=""
                SubList['userSub'] = ""
        
        #終了する必要があるか判別
        with end_flag.get_lock():
            loop_flag = end_flag.value
        time.sleep(0.1)

#Process2関連 / OpenAI問い合わせ
def OpenAI_Stream(key,talk_str,gpt_result_list,MemoryList,max_retries = 3):
    openai.api_key = key
     
    #メモリ容量
    #1メモリ=１応答,メモリー量は、応答時間およびToken Maxを考慮すること
    memory_capacity = 4
    memory_capacity = memory_capacity *2
    
    process_time=time.time()
    ###Promptの読み込み###
    prompts={}
    current_key=""
    
    ##システムプロンプトの取得
    #txtファイルよりプロンプトの取得
    file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"prompt.txt")
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file: 
            if line.strip().endswith("::"):
                current_key = line.strip()[:-2]
                current_key = current_key.strip() #空欄の消去
                prompts[current_key] = ""
            else:
                if line != '' or line != '\n':
                    prompts[current_key] += line.strip() + "\n"
    #システムプロンプトの追加
    ChatGPT_Prompt=[{"role": "system", "content": prompts.get("system")}]
    #メモリープロンプトの処理
    if len(MemoryList)!=0:
        #メモリ容量を超えたデータは破棄する。
        while len(MemoryList) > memory_capacity:
            MemoryList.pop(0) #切り取り
        ChatGPT_Prompt+=list(MemoryList) #メモリデータプロンプトの追加
        print("メモリ内容:",list(MemoryList))
    
    #取得した会話内容をユーザープロンプトとして追加
    ChatGPT_Prompt.append({"role": "user", "content": talk_str})
    
    #メモリーにユーザー読み上げデータの追加
    MemoryList.append({"role": "user", "content": talk_str})
    
    #print_color("#####プロンプト#####","yellow")  
    #for prompt in ChatGPT_Prompt:
    #    print_color(f"[{{'content': '{prompt['content']}'}}]","yellow")
    
    ###OpenAI Stream 実行###
    process_time=time.time()
    for i in range(max_retries):
        try:
            for chunk in openai.ChatCompletion.create(
                model="gpt-4", 
                messages=ChatGPT_Prompt,
                stream=True,
            ): 
                content = chunk["choices"][0].get("delta", {}).get("content")
                if content is not None:
                    print_color(f"{content}",'green',True)
                    gpt_result_list.put(content)
            # 成功したらループを抜ける
            break

        except openai.error.RateLimitError:
            # エラーメッセージを表示
            print_color(f"モデルが他のリクエストで過負荷となっています。")
            # エラーが持続する場合はOpenAIのヘルプセンターに連絡を促すメッセージを表示
            print_color(f"エラーが持続する場合は、help.openai.com に連絡してください。")
            # リトライの間隔を設定（ここでは例として1秒）
            time.sleep(1)
        except Exception as e:
            # その他のエラーを表示
            print_color(f"OpenAI問い合わせ中に、予期せぬエラーが発生しました：{e}")
            break
    print_color(f" / GPT4問い合わせ時間: {(time.time()-process_time):.1f}sec","yellow")
    #メモリーをテキストに記録
    with open('memory.txt', 'w',encoding='utf-8') as f:
        for item in MemoryList:
            f.write(json.dumps(item, ensure_ascii=False))
            f.write('\n')

#Process3 / GPTの出力を監視し、読み上げ文章があるときは加工して読み上げと表情変更の関数を作成する。
def convert_mirai_subGPT(gpt_result_list,VoiceVox_text_list,end_flag,MemoryList,shared_emotion_value,SubList,speakflag): 
    punctuation_marks = ("。", "？", "！",'!','?','"',')')
    VoiceVoxText=""
    emotion=""
    memoryText=""
    obs_mirai_sub = ""
    Mode_is = 0
    Readonly_mode=0
    VoiceVoxMode=1
    emotionMode=2
    
    with end_flag.get_lock():
        loop_flag = end_flag.value
     
    while not loop_flag:
        while not gpt_result_list.empty():
            #読み上げ文章がある
            TempText=gpt_result_list.get() #GPT出力テキストの取得
            memoryText+=TempText #一時メモリにテキストを追加

            #GPT出力終了記号検知
            if "}" in TempText:

                #メモリへGPT出力内容を追加
                MemoryList.append({"role": "assistant", "content": memoryText})
                print_color(f"メモリー追加:{memoryText}","green")
                memoryText = ""

                #英語字幕の表示
                DeepL_time=time.time()
                EN_sub = translate_client(obs_mirai_sub,'EN','JA').replace('\n', '')
                transrate_time = time.time()-DeepL_time
                if transrate_time < 2:
                    time.sleep(2-transrate_time)
                else:
                    print_color(f'\n<注意>翻訳時間が長い可能性があります。\nDeepLTranslate Time:{time.time()-DeepL_time}sec\n{EN_sub}',"yellow")
                obs.text_change(sceneName=scene_name,inputName="subs",inputSettings={"text":EN_sub},overlay=False,subDisplayAnchor='bottom',subDisplayOffset=36,fontsize=48,bk_color=14772545,bk_opacity=60)
                obs_mirai_sub = ""

                Mode_is =Readonly_mode
                continue
            #特殊記号の時は無視 
            elif any(mark in TempText for mark in ("{",":",'"',"->","assistant")):
                continue
            #contentの場合は、VoiceVox音声読み上げモード
            elif TempText == "content":
                Mode_is = VoiceVoxMode

                # 表情データの送信 (プロンプトにて、emotionとcontentの順番が変わった場合変更必要)
                shared_emotion_value["emotion"] = emotion
                emotion = ""
                continue
            #emotionの場合は、VtubeStudio表情変更モード
            elif TempText == "emotion":
                Mode_is = emotionMode
                continue
            
            if Mode_is == VoiceVoxMode:
                #ViceVox 声質の受信
                speakflag.wait()
                #VoiceVox Mode
                VoiceVoxText+=TempText
                obs_mirai_sub+=TempText
                
                #punctuation_marksを参照し、対象の記号が含まれる場合、VoiceVoxによる読み上げを行う
                #print(VoiceVoxText)
                if any(mark in VoiceVoxText for mark in punctuation_marks):
                    VoiceVox_text_list.put(VoiceVoxText) #VoiceVoxで読み上げるテキストをリストに追加
                    #OBS字幕の表示
                    SubList['miraiSub']=obs_mirai_sub
                    obs.text_change(sceneName=scene_name,inputName="subs",inputSettings={"text":obs_mirai_sub},overlay=False,subDisplayAnchor='bottom',subDisplayOffset=36,fontsize=48,bk_color=14772545,bk_opacity=60)
                    VoiceVoxText=""
            elif Mode_is == emotionMode:
                #emotion mode
                emotion+=TempText
            elif Mode_is == Readonly_mode:
                continue
            
        #終了する必要があるか判別
        with end_flag.get_lock():
            loop_flag = end_flag.value
        time.sleep(0.1)

#Process4 VoiceVox読み上げプロセス
def VoiceVoxPlayVoice(VoiceVox_text_list,end_flag,charactor_preset):
    with end_flag.get_lock():
        loop_flag = end_flag.value
    stream = audio_stream_start()
    while not loop_flag:
        while not VoiceVox_text_list.empty():
            #音声読み上げの実施
            Speech_text=VoiceVox_text_list.get()
            wavedata=text_to_wave(Speech_text,preset_id=0,speaker=charactor_preset["style_id"])
            stream.write(wavedata)
            time.sleep(0.01)
        #終了する必要があるか判別
        with end_flag.get_lock(): 
            loop_flag = end_flag.value
        time.sleep(0.1)
    audio_stream_stop(stream)

#Process4
def audio_stream_start():
    p = pyaudio.PyAudio()

    # VB-CABLEのインデックスを見つける
    current_os = platform.system()
    mic_name =""
    vb_cable_index = -1
    if current_os == "Windows":
        mic_name = "CABLE Input"
    else:
        mic_name = "VB"

    for i in range(p.get_device_count()):
        dev_info = p.get_device_info_by_index(i)
        #print(dev_info['name'])
        if mic_name in dev_info["name"]:
            vb_cable_index = dev_info["index"]
            #print(f'再生デバイス名: {dev_info["name"]}')
            break

    if vb_cable_index == -1:
        print("VB-Audio Virtualが見つかりませんでした。インストール済みかどうか確認してください。")
        return
    
    # ストリームを開く
    stream = p.open(format=pyaudio.paInt16,
                    channels=1,
                    rate=24000,
                    output=True,
                    output_device_index=vb_cable_index)
    #プツ音防止の為、待機
    time.sleep(0.4)
    return stream
#Process4
def audio_stream_stop(stream):
    p = pyaudio.PyAudio()
    stream.stop_stream()
    stream.close()

    p.terminate()

#Process5
def background_GPT(OpenAI_key,shared_emotion_value,end_flag,speak_flag,charactor_preset):
    openai.api_key=OpenAI_key
    current_emotion=""

    with end_flag.get_lock():
        loop_flag = end_flag.value
    
    while not loop_flag:
        emotion = shared_emotion_value['emotion']

        #VTS表情+声ピッチに関する問い合わせ実施
        if current_emotion != emotion:
            asyncio.run(async_func(openai_key,emotion,speak_flag,charactor_preset))
            current_emotion = emotion
        
        #終了する必要があるか判別
        with end_flag.get_lock():
            loop_flag = end_flag.value
        time.sleep(0.1)

async def async_func(openai_key,emotion,speak_flag,charactor_preset):
    print_color(f'Emotion:{emotion}','yellow')

    result1 = VoiceVoxGPT_AutoPich(openai_key,emotion)
    if result1 != True:
        Create_AutoPich_Preset(charactor_preset,result1)
    print_color(f'VoiceControl:{result1}','yellow')
    speak_flag.set()

    result2 = VtubeStudio_GPTmotion_Stream(openai_key,emotion)
    print_color(f'VTSControl:{result2}','yellow')
    if result2 != True:
        await VtubeStudio_emotion(result2,200)

#Process6 関連
def on_press(key, key_flag):
    try:
        if key == keyboard.Key.space:
            with key_flag.get_lock():
                key_flag.value = True
                print_color("space","green")
    except AttributeError:
        pass
    time.sleep(0.1)

#Process6 関連
def on_release(key, key_flag,end_flag):
    if key == keyboard.Key.esc:
        with end_flag.get_lock():
            end_flag.value = True
    time.sleep(0.1)

#Process6 / キーボード入力を監視 
def monitor_key(key_flag,end_flag):
    with keyboard.Listener(on_press=lambda key: on_press(key, key_flag), on_release=lambda key: on_release(key, key_flag,end_flag)) as listener:
        listener.join()

# multiprocessingで音声認識を実行する
if __name__ == "__main__":
    #キャラクタ設定
    charactor_preset={
        'CharacterName': 'みらい',
        'speaker_uuid': '882a636f-3bac-431a-966d-c5e6bba9f949',
        'style_id':48,
        'speakingSpeed':{'min':0.9,'default':1.0,'max':1.1},
        'pitch':{'min':-0.12,'default':-0.04,'max':0.01},
        'intonation':{'min':0.8,'default':1.3,'max':1.4}
        }
    #ネットワーク接続の確認
    response = os.system("ping -n 1 google.com > nul 2>&1") # Windowsの場合
    if response == 0:
        print_color("Internet Connection is Available",'green')
    else:
        print_color("Internet Connection is Not Available")
        sys.exit()
    
    #VTS認証確認
    asyncio.get_event_loop().run_until_complete(VtubeStudio_API.VTS_main().Authentication_Check())

    #共有配列の作成(出し入れあり)
    GPT_reslut_list = multiprocessing.Queue()
    VoiceVox_text_list = multiprocessing.Queue()

    #共有配列を作成
    MemoryList = multiprocessing.Manager().list()

    SubList = multiprocessing.Manager().dict()
    SubList["miraiSub"]=''
    SubList['userSub']=''

    shared_emotion_value = multiprocessing.Manager().dict()
    shared_emotion_value["emotion"] = ""

    #音声認識を止めるためのフラグ
    Speak_Flag = multiprocessing.Event()
    stop_event = multiprocessing.Event()
    
    #共有boolの作成
    key_flag = multiprocessing.Value(ctypes.c_bool, False)
    end_flag = multiprocessing.Value(ctypes.c_bool, False)
    
    #プロセスの作成 
    process1 = multiprocessing.Process(target=recognize_speech, args=(speech_key, speech_region, SubList, stop_event,))
    process2 = multiprocessing.Process(target=gpt_process, args=(SubList, GPT_reslut_list, openai_key, key_flag, end_flag,MemoryList,))
    process3 = multiprocessing.Process(target=convert_mirai_subGPT, args=(GPT_reslut_list,VoiceVox_text_list,end_flag,MemoryList,shared_emotion_value,SubList,Speak_Flag,))
    process4 = multiprocessing.Process(target=VoiceVoxPlayVoice, args=(VoiceVox_text_list,end_flag,charactor_preset,))
    process5 = multiprocessing.Process(target=background_GPT, args=(openai_key,shared_emotion_value,end_flag,Speak_Flag,charactor_preset,))
    process6 = multiprocessing.Process(target=monitor_key, args=(key_flag,end_flag))
    process1.start()
    process2.start()
    process3.start()
    process4.start()
    process5.start()
    process6.start()
    
    while end_flag.value == False:
        time.sleep(0.5)
    
    stop_event.set()
    print("終了")

    process1.join()
    process2.join()
    process3.join()
    process4.join()
    process5.join()
    process6.join()
main.py
Mega-Gorilla/AITuber_Mirai_Chan_v1.0