diff --git a/src/background.ts b/src/background.ts index b2778eb2dc..eed5659919 100644 --- a/src/background.ts +++ b/src/background.ts @@ -233,6 +233,10 @@ const store = new Store<{ enum: ["UTF-8", "Shift_JIS"], default: "UTF-8", }, + fileNamePattern: { + type: "string", + default: "", + }, fixedExportEnabled: { type: "boolean", default: false }, avoidOverwrite: { type: "boolean", default: false }, fixedExportDir: { type: "string", default: "" }, @@ -244,6 +248,7 @@ const store = new Store<{ }, default: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, avoidOverwrite: false, fixedExportDir: "", diff --git a/src/components/FileNamePatternDialog.vue b/src/components/FileNamePatternDialog.vue new file mode 100644 index 0000000000..63bac543c6 --- /dev/null +++ b/src/components/FileNamePatternDialog.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/components/SettingDialog.vue b/src/components/SettingDialog.vue index 793a69532a..7a3eb0494b 100644 --- a/src/components/SettingDialog.vue +++ b/src/components/SettingDialog.vue @@ -293,7 +293,6 @@ self="center right" transition-show="jump-left" transition-hide="jump-right" - v-if="!savingSetting.fixedExportEnabled" > 音声ファイルを設定したフォルダに書き出す @@ -343,6 +342,44 @@ + + + +
書き出しファイル名パターン
+
+ + + 書き出すファイル名のパターンをカスタマイズする + + +
+ +
+ {{ savingSetting.fileNamePattern }} +
+ +
+
上書き防止
@@ -680,10 +717,15 @@ import { ActivePointScrollMode, SplitTextWhenPasteType, } from "@/type/preload"; +import FileNamePatternDialog from "./FileNamePatternDialog.vue"; export default defineComponent({ name: "SettingDialog", + components: { + FileNamePatternDialog, + }, + props: { modelValue: { type: Boolean, @@ -958,6 +1000,8 @@ export default defineComponent({ store.dispatch("SET_SPLIT_TEXT_WHEN_PASTE", { splitTextWhenPaste }); }; + const showsFilePatternEditDialog = ref(false); + return { settingDialogOpenedComputed, engineMode, @@ -979,6 +1023,7 @@ export default defineComponent({ acceptRetrieveTelemetryComputed, splitTextWhenPaste, changeSplitTextWhenPaste, + showsFilePatternEditDialog, }; }, }); @@ -1015,6 +1060,12 @@ export default defineComponent({ background: colors.$primary; } +.text-ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .scroll-mode-button:hover { background: rgba(colors.$primary-rgb, 0.2); } diff --git a/src/store/audio.ts b/src/store/audio.ts index fdbe6bd429..8fdef5311e 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -29,11 +29,12 @@ import { import Encoding from "encoding-japanese"; import { PromiseType } from "./vuex"; import { + buildFileNameFromRawData, buildProjectFileName, convertHiraToKana, convertLongVowel, createKanaRegex, - sanitizeFileName, + currentDateString, } from "./utility"; async function generateUniqueIdAndQuery( @@ -103,9 +104,12 @@ function parseTextFile( } function buildFileName(state: State, audioKey: string) { + const fileNamePattern = state.savingSetting.fileNamePattern; + const index = state.audioKeys.indexOf(audioKey); const audioItem = state.audioItems[audioKey]; let styleName: string | undefined = ""; + const character = state.characterInfos?.find((info) => { const result = info.metas.styles.findIndex( (style) => style.styleId === audioItem.styleId @@ -122,20 +126,13 @@ function buildFileName(state: State, audioKey: string) { throw new Error(); } - const characterName = sanitizeFileName(character.metas.speakerName); - let text = sanitizeFileName(audioItem.text); - if (text.length > 10) { - text = text.substring(0, 9) + "…"; - } - - const preFileName = (index + 1).toString().padStart(3, "0"); - // デフォルトのスタイルだとstyleIdが定義されていないのでundefinedになる。なのでファイル名に入れてしまうことを回避する目的で分岐させています。 - if (styleName === undefined) { - return preFileName + `_${characterName}_${text}.wav`; - } - - const sanitizedStyleName = sanitizeFileName(styleName); - return preFileName + `_${characterName}(${sanitizedStyleName})_${text}.wav`; + return buildFileNameFromRawData(fileNamePattern, { + characterName: character.metas.speakerName, + index, + styleName, + text: audioItem.text, + date: currentDateString(), + }); } const audioBlobCache: Record = {}; diff --git a/src/store/setting.ts b/src/store/setting.ts index ace681b83a..bbd209ea4c 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -24,6 +24,7 @@ const hotkeyFunctionCache: Record HotkeyReturnType> = {}; export const settingStoreState: SettingStoreState = { savingSetting: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, fixedExportDir: "", avoidOverwrite: false, diff --git a/src/store/utility.ts b/src/store/utility.ts index c9aad029b2..3672b71a33 100644 --- a/src/store/utility.ts +++ b/src/store/utility.ts @@ -45,6 +45,83 @@ export function buildProjectFileName(state: State, extension?: string): string { : defaultFileNameStem; } +export const replaceTagIdToTagString = { + index: "連番", + characterName: "キャラ", + styleName: "スタイル", + text: "テキスト", + date: "日付", +}; +const replaceTagStringToTagId: { [tagString: string]: string } = Object.entries( + replaceTagIdToTagString +).reduce((prev, [k, v]) => ({ ...prev, [v]: k }), {}); + +export const DEFAULT_FILE_NAME_TEMPLATE = + "$連番$_$キャラ$($スタイル$)_$テキスト$.wav"; +const DEFAULT_FILE_NAME_VARIABLES = { + index: 0, + characterName: "四国めたん", + text: "テキストテキストテキスト", + styleName: "ノーマル", + date: currentDateString(), +}; + +export function currentDateString(): string { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = currentDate.getMonth().toString().padStart(2, "0"); + const date = currentDate.getDate().toString().padStart(2, "0"); + + return `${year}${month}${date}`; +} + +function replaceTag( + template: string, + replacer: { [key: string]: string } +): string { + const result = template.replace(/\$(.+?)\$/g, (match, p1) => { + const replaceTagId = replaceTagStringToTagId[p1]; + if (replaceTagId === undefined) { + return match; + } + return replacer[replaceTagId] ?? ""; + }); + + return result; +} + +export function buildFileNameFromRawData( + fileNamePattern = DEFAULT_FILE_NAME_TEMPLATE, + vars = DEFAULT_FILE_NAME_VARIABLES +): string { + let pattern = fileNamePattern; + if (pattern === "") { + // ファイル名指定のオプションが初期値("")ならデフォルトテンプレートを使う + pattern = DEFAULT_FILE_NAME_TEMPLATE; + } + + let text = sanitizeFileName(vars.text); + if (text.length > 10) { + text = text.substring(0, 9) + "…"; + } + + const characterName = sanitizeFileName(vars.characterName); + + const index = (vars.index + 1).toString().padStart(3, "0"); + + const styleName = sanitizeFileName(vars.styleName); + + const date = currentDateString(); + + return replaceTag(pattern, { + index, + characterName, + styleName: styleName, + text, + date, + }); +} + export const getToolbarButtonName = (tag: ToolbarButtonTagType): string => { const tag2NameObj: Record = { PLAY_CONTINUOUSLY: "連続再生", diff --git a/src/type/preload.d.ts b/src/type/preload.d.ts index 46dbe03bb4..62a4d704ab 100644 --- a/src/type/preload.d.ts +++ b/src/type/preload.d.ts @@ -146,6 +146,7 @@ export type SplitTextWhenPasteType = "PERIOD_AND_NEW_LINE" | "NEW_LINE" | "OFF"; export type SavingSetting = { exportLab: boolean; fileEncoding: Encoding; + fileNamePattern: string; fixedExportEnabled: boolean; fixedExportDir: string; avoidOverwrite: boolean; diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index e32308ceca..22b73beab6 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -46,6 +46,7 @@ describe("store/vuex.js test", () => { savedLastCommandUnixMillisec: null, savingSetting: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, fixedExportDir: "", avoidOverwrite: false, @@ -161,6 +162,7 @@ describe("store/vuex.js test", () => { assert.propertyVal(store.state.savingSetting, "fixedExportDir", ""); assert.propertyVal(store.state.savingSetting, "avoidOverwrite", false); assert.propertyVal(store.state.savingSetting, "exportLab", false); + assert.propertyVal(store.state.savingSetting, "fileNamePattern", ""); assert.equal(store.state.isPinned, false); assert.isObject(store.state.presetItems); assert.isEmpty(store.state.presetItems);