├── Editor ├── Utility.cs.meta ├── TextMeshProRubyInspector.cs.meta ├── TextMeshproRubyEditorConfig.cs.meta ├── TextMeshProRubyISettingsProvider.cs.meta ├── Prompt.txt.meta ├── TextMeshProRuby-Editor.asmdef.meta ├── Prompt.txt ├── TextMeshProRuby-Editor.asmdef ├── TextMeshproRubyEditorConfig.cs ├── TextMeshProRubyInspector.cs ├── TextMeshProRubyISettingsProvider.cs └── Utility.cs ├── Runtime ├── TMProRubyUtil.cs.meta ├── TextMeshProRuby.cs.meta ├── TextMeshProRuby.asmdef.meta ├── TextMeshProRuby.asmdef ├── TextMeshProRuby.cs └── TMProRubyUtil.cs ├── CHANGELOG.md.meta ├── LICENSE.txt.meta ├── README.md.meta ├── package.json.meta ├── Editor.meta ├── Runtime.meta ├── package.json ├── CHANGELOG.md ├── README.md └── LICENSE.txt /Editor/Utility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb73a879ae36f4d24ab907d6156de2b8 3 | -------------------------------------------------------------------------------- /Runtime/TMProRubyUtil.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 88e9ca2cb45cc4b2a889318eef3a5944 -------------------------------------------------------------------------------- /Runtime/TextMeshProRuby.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a8765114703be401999244387c19feb0 -------------------------------------------------------------------------------- /Editor/TextMeshProRubyInspector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e1e05034ec7b41eab530b3d37751a3f -------------------------------------------------------------------------------- /Editor/TextMeshproRubyEditorConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4197dc28cce93447f8eb21b2c81f0f73 3 | -------------------------------------------------------------------------------- /Editor/TextMeshProRubyISettingsProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa4e9aaf21bdc49828a47d23762a5ba4 3 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 90716c9f7eb2f4149b6ca775d3fa22cb 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 577687c6bcd314570ae8de9b2c1c1dec 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 953f6d23c57b8459ea66c68330f7a116 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a56052ff3bf6b4f17ae58c5826cd70af 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Prompt.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d0fd6e75e0a5443bb993e078985385e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0598cfe55ca6d4e7b8d9293c9d05ab31 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a4d1837e403e94a9e9db69b78c5e2d0b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/TextMeshProRuby.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 52f7a850cce134e7a86ee27f8e05c034 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/TextMeshProRuby-Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f32137a77b1cc40c7b9e21408d2bb055 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jp.amagamina.text-mesh-pro-ruby", 3 | "version": "1.1.0", 4 | "displayName": "TextMeshProRuby", 5 | "description": "Japanese Ruby (Furigana) component.", 6 | "author": "ina-amagami ", 7 | "license": "MIT" 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.0] - 2025-05-31 4 | 5 | OpenAIのAPIによるルビタグ生成をサポート 6 | 7 | ### Changed 8 | - UPMパッケージ対応。それに伴いAssemblyDefinitionとnamespace追加 9 | 10 | ### Added 11 | - 漢字部分に対して空のルビタグを付与する機能を追加 12 | 13 | ## [1.0.0] - 2019-12-19 14 | 15 | 初期バージョン 16 | -------------------------------------------------------------------------------- /Editor/Prompt.txt: -------------------------------------------------------------------------------- 1 | タグ付きの入力テキストに対して、以下の処理をしてください。 2 | 3 | ・漢字 の形式になっている箇所に、`=` の後に正しいひらがなの読みを補完してください。 4 | ・読みは日本語の自然な文脈に合わせて付けてください。 5 | ・既にひらがなが入っている場合はそのままにしてください。 6 | ・出力には変換後の文章だけを含め、前後に説明などは不要です。 7 | 8 | ## 入力例 9 | 10 | TextMeshProの漢字テキストにルビを振るコンポーネント 11 | 12 | ## 出力例 13 | 14 | TextMeshProの漢字テキストにルビを振るコンポーネント 15 | 16 | ## 入力テキスト 17 | 18 | {srcText} -------------------------------------------------------------------------------- /Runtime/TextMeshProRuby.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TextMeshProRuby", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:6055be8ebefd69e48b49212b09b47b2f" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/TextMeshProRuby-Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TextMeshProRuby-Editor", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:52f7a850cce134e7a86ee27f8e05c034" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextMeshProRuby 2 | 3 | ![TextMeshProRuby](https://amagamina.jp/wp-content/uploads/2019/12/tmpro-ruby-test.gif) 4 | 5 | UnityのTextMeshProで漢字テキストにルビを振る機能です。 6 | 7 | v1.1.0より、OpenAIのAPIによるルビタグ自動生成のサポートを追加しました。 8 | 9 | ![ルビタグAI生成](https://amagamina.jp/blog/wp-content/uploads/2025/05/tmpro-ruby-ai.gif) 10 | 11 | 詳しい解説は[**こちら**](https://amagamina.jp/tmpro-ruby/) 12 | 13 | ## インストール 14 | 15 | upm経由でインストールする場合は `https://github.com/ina-amagami/TextMeshProRuby.git` を指定して下さい。 16 | 17 | ```manifest.json 18 | { 19 | "dependencies": { 20 | "jp.amagamina.text-mesh-pro-ruby": "https://github.com/ina-amagami/TextMeshProRuby.git" 21 | } 22 | } 23 | ``` 24 | 25 | ## ライセンス条項 26 | 27 | MITライセンス 28 | https://opensource.org/licenses/mit-license.php 29 | 30 | コード内のライセンス表記を残して頂ければ自由に使用可能です。 31 | 32 | Copyright (c) 2019-2025 ina-amagami (ina@amagamina.jp) 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 ina-amagami (ina@amagamina.jp) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Editor/TextMeshproRubyEditorConfig.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System.IO; 4 | 5 | namespace TMP_Ruby.Editor 6 | { 7 | public class TextMeshProRubyEditorConfig : ScriptableObject 8 | { 9 | [Header("ルビタグ付与対象の正規表現")] 10 | public string KanjiRegex = @"[\u3040-\u309F\u30A0-\u30FFA-Za-z0-9a-zA-Z0-9ヲ-゚\s、。!?「」()『』【】\r\n\t]"; 11 | 12 | [Header("AIルビ振りを有効化")] public bool EnableRubyFromAI = true; 13 | 14 | [Header("OpenAI API Settings")] public string OpenAI_Model = "gpt-4o"; 15 | public string OpenAI_APIKey; 16 | [Range(0, 1f)] public float OpenAI_Temperature = 0f; 17 | 18 | private const string AssetFileName = nameof(TextMeshProRubyEditorConfig) + ".asset"; 19 | private const string AssetFolder = "Assets/Editor Default Resources"; 20 | private const string AssetPath = AssetFolder + "/" + AssetFileName; 21 | 22 | private static TextMeshProRubyEditorConfig _instance; 23 | 24 | public static TextMeshProRubyEditorConfig Instance 25 | { 26 | get 27 | { 28 | if (_instance == null) 29 | { 30 | LoadOrCreateAsset(); 31 | } 32 | 33 | return _instance; 34 | } 35 | } 36 | 37 | private static void LoadOrCreateAsset() 38 | { 39 | _instance = AssetDatabase.LoadAssetAtPath(AssetPath); 40 | if (_instance != null) 41 | return; 42 | 43 | if (_instance != null) 44 | { 45 | return; 46 | } 47 | 48 | _instance = CreateInstance(); 49 | if (!Directory.Exists(AssetFolder)) 50 | { 51 | Directory.CreateDirectory(AssetFolder); 52 | } 53 | 54 | AssetDatabase.CreateAsset(_instance, 55 | Path.Combine(AssetFolder, nameof(TextMeshProRubyEditorConfig) + ".asset")); 56 | AssetDatabase.SaveAssets(); 57 | AssetDatabase.Refresh(); 58 | 59 | Debug.Log("Created new TextMeshProRubyEditorConfig in Editor Default Resources."); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Editor/TextMeshProRubyInspector.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace TMP_Ruby.Editor 5 | { 6 | [CustomEditor(typeof(TextMeshProRuby))] 7 | public class TextMeshProRubyEditor : UnityEditor.Editor 8 | { 9 | TextMeshProRuby tmProRuby; 10 | SerializedObject so; 11 | SerializedProperty fixedLineHeightProp; 12 | SerializedProperty autoMarginTopProp; 13 | 14 | private void OnEnable() 15 | { 16 | tmProRuby = target as TextMeshProRuby; 17 | so = new SerializedObject(target); 18 | fixedLineHeightProp = so.FindProperty("fixedLineHeight"); 19 | autoMarginTopProp = so.FindProperty("autoMarginTop"); 20 | } 21 | 22 | public override void OnInspectorGUI() 23 | { 24 | base.OnInspectorGUI(); 25 | 26 | so.Update(); 27 | EditorGUI.BeginChangeCheck(); 28 | 29 | EditorGUILayout.PropertyField(fixedLineHeightProp); 30 | 31 | if (GUILayout.Button("漢字にタグ付与")) 32 | { 33 | tmProRuby.Text = Utility.AddRubyTagToKanji(tmProRuby.Text); 34 | GUI.FocusControl(null); 35 | EditorUtility.SetDirty(tmProRuby); 36 | } 37 | 38 | if (TextMeshProRubyEditorConfig.Instance.EnableRubyFromAI && GUILayout.Button("AIルビ振り")) 39 | { 40 | // APIキーが空の場合、設定画面を開く 41 | if (string.IsNullOrEmpty(TextMeshProRubyEditorConfig.Instance.OpenAI_APIKey)) 42 | { 43 | EditorUtility.DisplayDialog( 44 | "OpenAIのAPIキーが未設定です", 45 | "AIルビ振り機能を使うにはOpenAI(ChatGPT)のAPIキーを設定する必要があります", 46 | "OK"); 47 | SettingsService.OpenProjectSettings("Project/TMP Ruby Settings"); 48 | } 49 | else 50 | { 51 | tmProRuby.Text = Utility.CreateRubyTagByOpenAI(tmProRuby.Text); 52 | GUI.FocusControl(null); 53 | EditorUtility.SetDirty(tmProRuby); 54 | } 55 | } 56 | 57 | if (fixedLineHeightProp.boolValue) 58 | { 59 | EditorGUI.indentLevel++; 60 | EditorGUILayout.PropertyField(autoMarginTopProp); 61 | EditorGUI.indentLevel--; 62 | } 63 | 64 | if (EditorGUI.EndChangeCheck()) 65 | { 66 | Undo.RegisterFullObjectHierarchyUndo(tmProRuby.gameObject, "TextMeshProRuby"); 67 | 68 | so.ApplyModifiedProperties(); 69 | if (tmProRuby.enabled) 70 | { 71 | tmProRuby.Apply(); 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Editor/TextMeshProRubyISettingsProvider.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace TMP_Ruby.Editor 5 | { 6 | static class TextMeshProRubySettingsProvider 7 | { 8 | private static SerializedObject _serializedObject; 9 | 10 | [SettingsProvider] 11 | public static SettingsProvider CreateTextMeshProRubySettingsProvider() 12 | { 13 | var provider = new SettingsProvider("Project/TMP Ruby Settings", SettingsScope.Project) 14 | { 15 | label = "TMP Ruby Settings", 16 | 17 | guiHandler = (searchContext) => 18 | { 19 | var config = TextMeshProRubyEditorConfig.Instance; 20 | 21 | if (_serializedObject == null || _serializedObject.targetObject != config) 22 | { 23 | _serializedObject = new SerializedObject(config); 24 | } 25 | 26 | EditorGUI.BeginDisabledGroup(true); 27 | EditorGUILayout.ObjectField("Config Asset", config, typeof(TextMeshProRubyEditorConfig), false); 28 | EditorGUI.EndDisabledGroup(); 29 | 30 | _serializedObject.Update(); 31 | 32 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("KanjiRegex")); 33 | 34 | EditorGUILayout.Separator(); 35 | EditorGUILayout.HelpBox("AI機能はエディタ専用です。ランタイムには含まれません", MessageType.Info); 36 | 37 | var enableAIProp = _serializedObject.FindProperty("EnableRubyFromAI"); 38 | EditorGUILayout.PropertyField(enableAIProp); 39 | 40 | EditorGUI.BeginDisabledGroup(!enableAIProp.boolValue); 41 | 42 | var modelProp = _serializedObject.FindProperty("OpenAI_Model"); 43 | EditorGUILayout.PropertyField(modelProp); 44 | 45 | var apiKeyProp = _serializedObject.FindProperty("OpenAI_APIKey"); 46 | EditorGUILayout.PropertyField(apiKeyProp); 47 | 48 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("OpenAI_Temperature")); 49 | 50 | EditorGUI.EndDisabledGroup(); 51 | 52 | if (enableAIProp.boolValue) 53 | { 54 | if (string.IsNullOrEmpty(modelProp.stringValue)) 55 | { 56 | EditorGUILayout.HelpBox("Modelが入力されていません", MessageType.Error); 57 | } 58 | 59 | if (string.IsNullOrEmpty(apiKeyProp.stringValue)) 60 | { 61 | EditorGUILayout.HelpBox("APIキーが入力されていません", MessageType.Error); 62 | } 63 | } 64 | 65 | _serializedObject.ApplyModifiedProperties(); 66 | }, 67 | 68 | keywords = new[] {"OpenAI", "TMP", "Ruby", "API", "Key", "Kanji"} 69 | }; 70 | 71 | return provider; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Runtime/TextMeshProRuby.cs: -------------------------------------------------------------------------------- 1 | /* 2 | TextMeshProRuby 3 | 4 | Copyright (c) 2019-2025 ina-amagami (ina@amagamina.jp) 5 | 6 | This software is released under the MIT License. 7 | https://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using UnityEngine; 11 | using TMPro; 12 | 13 | namespace TMP_Ruby 14 | { 15 | [RequireComponent(typeof(TMP_Text))] 16 | public class TextMeshProRuby : MonoBehaviour 17 | { 18 | [SerializeField, HideInInspector] private TMP_Text tmpText; 19 | 20 | [TextArea(5, 10)] [Tooltip("ルビは 文字 もしくは 文字")] [SerializeField] 21 | private string text; 22 | 23 | /// 24 | /// ルビタグを含んだテキスト 25 | /// 26 | public string Text 27 | { 28 | get => text; 29 | set 30 | { 31 | text = value; 32 | if (enabled) 33 | { 34 | Apply(); 35 | } 36 | } 37 | } 38 | 39 | [Tooltip("行間を固定します")] [SerializeField, HideInInspector] 40 | private bool fixedLineHeight; 41 | 42 | /// 43 | /// 行間を固定する 44 | /// 45 | public bool FixedLineHeight 46 | { 47 | get => fixedLineHeight; 48 | set 49 | { 50 | bool isChanged = fixedLineHeight != value; 51 | fixedLineHeight = value; 52 | if (isChanged && enabled) 53 | { 54 | Apply(); 55 | } 56 | } 57 | } 58 | 59 | [Tooltip("1行目のルビ有無によって自動でMarginTopを追加します")] [SerializeField, HideInInspector] 60 | private bool autoMarginTop = true; 61 | 62 | /// 63 | /// 1行目のルビ有無によって自動でMarginTopを追加する 64 | /// 65 | public bool AutoMarginTop 66 | { 67 | get => autoMarginTop; 68 | set 69 | { 70 | bool isChanged = autoMarginTop != value; 71 | autoMarginTop = value; 72 | if (isChanged && enabled) 73 | { 74 | Apply(); 75 | } 76 | } 77 | } 78 | 79 | private void OnEnable() 80 | { 81 | Apply(); 82 | } 83 | 84 | public void Apply() 85 | { 86 | if (!tmpText) 87 | { 88 | tmpText = GetComponent(); 89 | } 90 | 91 | tmpText.SetTextAndExpandRuby(Text, fixedLineHeight, autoMarginTop); 92 | } 93 | 94 | #if UNITY_EDITOR 95 | private void Reset() 96 | { 97 | tmpText = GetComponent(); 98 | Text = tmpText.text; 99 | } 100 | 101 | private void OnValidate() 102 | { 103 | // Copy & PasteComponent対応 104 | var newTMPText = GetComponent(); 105 | if (tmpText != newTMPText) 106 | { 107 | tmpText = newTMPText; 108 | Text = tmpText.text; 109 | return; 110 | } 111 | 112 | if (enabled) 113 | { 114 | Apply(); 115 | } 116 | } 117 | #endif 118 | } 119 | } -------------------------------------------------------------------------------- /Runtime/TMProRubyUtil.cs: -------------------------------------------------------------------------------- 1 | /* 2 | TextMeshProRuby 3 | 4 | Copyright (c) 2019-2025 ina-amagami (ina@amagamina.jp) 5 | 6 | This software is released under the MIT License. 7 | https://opensource.org/licenses/mit-license.php 8 | */ 9 | 10 | using System.Text; 11 | using System.Text.RegularExpressions; 12 | using TMPro; 13 | 14 | namespace TMP_Ruby 15 | { 16 | public static class TMProRubyUtil 17 | { 18 | private static readonly Regex TagRegex = 19 | new Regex(".*?)\"?>(?.*?)", RegexOptions.IgnoreCase); 20 | 21 | /// 22 | /// 展開後の開始タグ 23 | /// 24 | private const string StartTag = ""; 25 | 26 | /// 27 | /// 展開後の終了タグ 28 | /// 29 | private const string EndTag = ""; 30 | 31 | /// 32 | /// GCAlloc対策 33 | /// 34 | private static readonly StringBuilder builder = new StringBuilder(StringBuilderCapacity); 35 | 36 | private const int StringBuilderCapacity = 1024; 37 | 38 | /// 39 | /// ルビタグを展開してセット 40 | /// 41 | public static void SetTextAndExpandRuby( 42 | this TMP_Text tmpText, 43 | string text, 44 | bool fixedLineHeight = false, 45 | bool autoMarginTop = true) 46 | { 47 | // 1行目にルビがあるか調べる 48 | var isFirstLineRuby = false; 49 | if (fixedLineHeight && autoMarginTop) 50 | { 51 | var firstNewLineIndex = text.IndexOf('\n'); 52 | var firstLine = firstNewLineIndex > 1 ? text.Substring(0, firstNewLineIndex + 1) : text; 53 | isFirstLineRuby = TagRegex.IsMatch(firstLine); 54 | } 55 | 56 | text = GetExpandText(text); 57 | 58 | if (fixedLineHeight) 59 | { 60 | // 行間を固定 61 | var lineHeight = tmpText.font.faceInfo.lineHeight / tmpText.font.faceInfo.pointSize; 62 | text = $"{text}"; 63 | 64 | // 1行目にルビがある時はMarginTopで位置調整 65 | if (autoMarginTop) 66 | { 67 | var margin = tmpText.margin; 68 | margin.y = isFirstLineRuby ? -(tmpText.fontSize * 0.55f) : 0; 69 | margin.y *= tmpText.isOrthographic ? 1 : 0.1f; 70 | tmpText.margin = margin; 71 | } 72 | } 73 | 74 | tmpText.text = text; 75 | } 76 | 77 | /// 78 | /// 文字列に含まれるルビタグを展開して取得 79 | /// 80 | /// ルビタグ展開後の文字列 81 | public static string GetExpandText(string text) 82 | { 83 | var match = TagRegex.Match(text); 84 | while (match.Success) 85 | { 86 | if (match.Groups.Count > 2) 87 | { 88 | builder.Length = 0; 89 | 90 | var ruby = match.Groups["ruby"].Value; 91 | var rL = ruby.Length; 92 | var kanji = match.Groups["kanji"].Value; 93 | var kL2 = kanji.Length * 2; 94 | 95 | // 手前に付ける空白 96 | var space = kL2 < rL ? (rL - kL2) * 0.25f : 0f; 97 | if (space < 0 || space > 0) 98 | { 99 | builder.Append($""); 100 | } 101 | 102 | // 漢字 - 文字数分だけ左に移動 - 開始タグ - ルビ - 終了タグ 103 | space = -(kL2 * 0.25f + rL * 0.25f); 104 | builder.Append($"{kanji}{StartTag}{ruby}{EndTag}"); 105 | 106 | // 後ろに付ける空白 107 | space = kL2 > rL ? (kL2 - rL) * 0.25f : 0f; 108 | if (space < 0 || space > 0) 109 | { 110 | builder.Append($""); 111 | } 112 | 113 | text = text.Replace(match.Groups[0].Value, builder.ToString()); 114 | } 115 | 116 | match = match.NextMatch(); 117 | } 118 | 119 | return text; 120 | } 121 | 122 | /// 123 | /// TextMeshPro以外などの表示用にルビタグを除外したテキストを取得 124 | /// 125 | /// ルビタグ削除の文字列 126 | public static string RemoveRubyTag(string text) 127 | { 128 | var match = TagRegex.Match(text); 129 | while (match.Success) 130 | { 131 | if (match.Groups.Count > 2) 132 | { 133 | text = text.Replace(match.Groups[0].Value, match.Groups["kanji"].Value); 134 | } 135 | 136 | match = match.NextMatch(); 137 | } 138 | 139 | return text; 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /Editor/Utility.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace TMP_Ruby.Editor 10 | { 11 | public static class Utility 12 | { 13 | /// 14 | /// 文章の漢字部分にrタグだけ付与する(ひらがなは付与しない) 15 | /// 16 | public static string AddRubyTagToKanji(string srcText) 17 | { 18 | var tagRegex = new Regex(@"]*?>.*?<\/r>", RegexOptions.Singleline); 19 | var matches = tagRegex.Matches(srcText); 20 | 21 | var protectedRanges = new List<(int start, int end)>(); 22 | foreach (Match match in matches) 23 | { 24 | protectedRanges.Add((match.Index, match.Index + match.Length)); 25 | } 26 | 27 | var result = new StringBuilder(); 28 | for (var i = 0; i < srcText.Length;) 29 | { 30 | // 現在の位置が保護範囲か? 31 | var inProtected = protectedRanges.Exists(r => i >= r.start && i < r.end); 32 | if (inProtected) 33 | { 34 | // 保護範囲にある → その範囲はそのまま追加 35 | var range = protectedRanges.Find(r => i >= r.start && i < r.end); 36 | result.Append(srcText.Substring(range.start, range.end - range.start)); 37 | i = range.end; 38 | } 39 | else 40 | { 41 | // 保護範囲外 → ルビ対象文字列を探す 42 | var match = MatchRubyTarget(srcText, i); 43 | if (match.length > 0) 44 | { 45 | result.Append($"{match.value}"); 46 | i += match.length; 47 | } 48 | else 49 | { 50 | result.Append(srcText[i]); 51 | i++; 52 | } 53 | } 54 | } 55 | 56 | return result.ToString(); 57 | } 58 | 59 | // ルビを付ける対象文字列のマッチ処理 60 | private static (string value, int length) MatchRubyTarget(string text, int start) 61 | { 62 | var i = start; 63 | while (i < text.Length) 64 | { 65 | if (Regex.IsMatch(text[i].ToString(), 66 | TextMeshProRubyEditorConfig.Instance.KanjiRegex)) 67 | { 68 | break; 69 | } 70 | 71 | i++; 72 | } 73 | 74 | return i > start ? (text.Substring(start, i - start), i - start) : ("", 0); 75 | } 76 | 77 | /// 78 | /// OpenAIのAPIで、rタグに対してひらがなを付与する 79 | /// 80 | public static string CreateRubyTagByOpenAI(string srcText) 81 | { 82 | var config = TextMeshProRubyEditorConfig.Instance; 83 | var model = config.OpenAI_Model; 84 | var apiKey = config.OpenAI_APIKey; 85 | var temperature = config.OpenAI_Temperature; 86 | 87 | var promptPath = Path.Combine(GetScriptDirectory(), "Prompt.txt"); 88 | if (!File.Exists(promptPath)) 89 | { 90 | Debug.LogError("Prompt.txt が見つかりません: " + promptPath); 91 | return srcText; 92 | } 93 | 94 | var prompt = File.ReadAllText(promptPath, Encoding.UTF8) 95 | .Replace("{srcText}", AddRubyTagToKanji(srcText)); 96 | 97 | try 98 | { 99 | var result = PostToOpenAISync(prompt, model, apiKey, temperature); 100 | return string.IsNullOrEmpty(result) ? srcText : result; 101 | } 102 | catch (Exception ex) 103 | { 104 | Debug.LogError("OpenAI API 呼び出し失敗: " + ex.Message); 105 | return srcText; 106 | } 107 | } 108 | 109 | private static string GetScriptDirectory( 110 | [System.Runtime.CompilerServices.CallerFilePath] 111 | string filePath = null) 112 | { 113 | return Path.GetDirectoryName(filePath); 114 | } 115 | 116 | private static string PostToOpenAISync(string prompt, string model, string apiKey, float temperature) 117 | { 118 | using var client = new HttpClient(); 119 | 120 | client.DefaultRequestHeaders.Authorization = 121 | new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey); 122 | 123 | var payload = new ChatCompletionRequest 124 | { 125 | model = model, 126 | messages = new[] 127 | { 128 | new Message {role = "user", content = prompt} 129 | }, 130 | temperature = temperature 131 | }; 132 | 133 | var content = new StringContent(payload.ToString(), Encoding.UTF8, "application/json"); 134 | 135 | var response = client.PostAsync("https://api.openai.com/v1/chat/completions", content).GetAwaiter() 136 | .GetResult(); 137 | var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); 138 | 139 | if (!response.IsSuccessStatusCode) 140 | { 141 | throw new Exception($"OpenAI API error: {response.StatusCode}\n{responseBody}"); 142 | } 143 | 144 | var parsed = JsonUtility.FromJson(responseBody); 145 | return parsed?.choices?[0]?.message?.content ?? ""; 146 | } 147 | 148 | [Serializable] 149 | internal class Message 150 | { 151 | public string role; 152 | public string content; 153 | } 154 | 155 | [Serializable] 156 | internal class ChatCompletionRequest 157 | { 158 | public string model; 159 | public Message[] messages; 160 | public float temperature; 161 | 162 | public override string ToString() 163 | { 164 | return JsonUtility.ToJson(this); 165 | } 166 | } 167 | 168 | [Serializable] 169 | internal class Choice 170 | { 171 | public Message message; 172 | } 173 | 174 | [Serializable] 175 | internal class ChatCompletionResponse 176 | { 177 | public Choice[] choices; 178 | } 179 | } 180 | } --------------------------------------------------------------------------------