pythonでmanimとvoicevoxを操って、ずんだもんの平面幾何解説動画を作ってみた。
さらにゆっくりムービーメーカーにも連携させますた。
ゆっくりムービーメーカー用の特殊台本ファイルも吐き出します。
import os
import re
import shutil
import __main__
import sys
import time
import math
from manim import *
Tex.set_default(tex_template=TexTemplate(
tex_compiler = "lualatex",
# tex_compiler = "luatex" でも可
output_format = ".pdf",
preamble = r"""
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{luatexja}
\usepackage[haranoaji]{luatexja-preset}
"""
))
import numpy as np
import requests
from pathlib import Path
from pydub import AudioSegment
def hhmmss(seconds):
h = int(seconds // 3600)
m = int(seconds % 3600 // 60)
s = seconds % 60
return f"{h:02}:{m:02}:{s:02.6}" # 結果: 2:46:40
def voicevox(text, output_path, speaker=2, speedScale=1.2):
url = "http://localhost:50021/audio_query"
params = {"text": text, "speaker": speaker}
timeout = 15
query_synthesis = requests.post(url, params=params, timeout=timeout)
json = query_synthesis.json()
json['speedScale'] = speedScale
json['prePhonemeLength'] = 0.3 #前に0.3秒の無音を追加
json['postPhonemeLength'] = 0.3 #後に0.3秒の無音を追加
response = requests.post(
"http://localhost:50021/synthesis",
params=params,
json=json,
)
out = Path(output_path)
out.write_bytes(response.content)
return AudioSegment.from_file(output_path, "wav")
def get_points(dots, dots_on_line):
#点の座標を取得
points = []
for char in dots_on_line:
points.append(dots[char])
return points
def get_internal_division_point(dots, dots_on_line,m=1,n=1):
#2点の座標を取得
points = get_points(dots,dots_on_line)
#2点をm:nに内分する点を返す
return (np.array(points[0])*n+np.array(points[1])*m)/(m+n)
def get_Foot_perpendicular(dots,dots_on_line , dot1):
#点dot1から2点を通る直線に下ろした垂線の足の求める
# 2点との垂直二等分線の適当な両端を求める
foot = perpendicular_bisector(get_points(dots, dots_on_line))
# dots_on_lineの両端と、dot1とdot1を通って垂直二等分線方向に移動した点の交点を求める
return line_intersection([dots[dot1], dots[dot1] + (foot[1] - foot[0])], get_points(dots, dots_on_line))
def get_distance(dots, dots_on_line):
# 2点の座標を取得
points = get_points(dots, dots_on_line)
distance = np.linalg.norm(points[1] - points[0])
return distance
def get_included_angle(dots, dots_on_line):
# 角を挟むLineを返す
points = []
for char in dots_on_line:
points.append(dots[char])
return Line(points[1], points[0]), Line(points[1], points[2])
def talk_voiv(self, speaker, text, dis_text=''):
global i_Voice
if dis_text == '':
dis_text=text
m = re.findall(r'(\d+)/(\d+)', text) # 文字列から数字にマッチするものをリストとして取得
for bunsu in m:
p1 = bunsu[0] + "/" + bunsu[1]
p2 = bunsu[1] + "ぶんの" + bunsu[0]
text = text.replace(p1, p2)
m = re.findall(r'(\d+)([::])(\d+)', text) # 文字列から数字にマッチするものをリストとして取得
for bunsu in m:
p1 = bunsu[0] + bunsu[1] + bunsu[2]
p2 = bunsu[0] + "たい" + bunsu[2]
text = text.replace(p1, p2)
text = text.replace('△','三角形')
text = text.replace('∠', 'かく')
text = re.sub('=|=', 'イコール',text)
text = re.sub('3・4・5の|345の', 'さんよんごの', text)
text = text.replace('①', '1まる')
text = text.replace('②', '2まる')
text = text.replace('③', '3まる')
text = text.replace('④', '4まる')
text = text.replace('⑤', '5まる')
text = text.replace('⑥', '6まる')
text = text.replace('⑦', '7まる')
text = text.replace('⑧', '8まる')
text = text.replace('⑨', '9まる')
text = text.replace('⑩', '10まる')
text = text.replace('√', 'ルート')
text = text.replace('底角', 'ていかく')
text = text.replace('頂角', 'ちょうかく')
text = text.replace('底辺', 'ていへん')
text = text.replace('斜辺', 'しゃへん')
text = text.replace('四角形', 'しかっけい')
text = text.replace('1辺', 'いっぺん')
text = text.replace('黄色', 'きいろ')
text = text.replace('情強', 'じょうきょう')
text = text.replace('印をつけ', 'しるしをつけ')
text = text.replace('捨て問', 'すてもん')
text = text.replace('勝ったな', 'かったな')
text = text.replace('辺', 'へん')
text = text.replace('-', 'マイナス')
text = text.replace('ad', 'エーディー')
text = text.replace('bc', 'ビーシー')
chcolor="#FFFFFF"
if (speaker==1) or (speaker==3):
chcolor = "#c0fc8a"
speaker_name = "ずんだもん"
elif speaker==2:
chcolor = "#f78bd2"
speaker_name = "四国めたん"
if f_voice_sub:
vg_text = Text(dis_text, font="Yu Gothic UI",font_size=50,color=chcolor)
self.add(vg_text.move_to([0, 5-config.frame_height, 0]))
sound = voicevox(text, f"media\\videos\\voicevox.wav", speaker, 1.2)
self.add_sound(f"media\\videos\\voicevox.wav")
t1=self.time
self.wait(sound.duration_seconds)
if f_voice_sub:
self.remove(vg_text)
t2 = self.time
f.write(hhmmss(t1) +','+hhmmss(t2)+'\n')
f1.write(speaker_name+',"'+text+'","'+dis_text+'",,'+str(int(t1*config.frame_rate))+",,\n")
print(hhmmss(t1),dis_text)
f.write(dis_text+'\n\n')
def clockpoti(x,r=1):
#点の名前とかを時計の位置に従ってずらすベクトルを生成
s =360+90-360/12 * x
return [round(math.cos(math.radians(s))*r,2),round(math.sin(math.radians(s))*r,2),0]
# レンダリング設定
config.frame_rate = 60
#config.background_color = DARK_GREY
config.format = "mp4"
config.quality = "high_quality"
config.frame_height = 23
config.frame_width = 23
#width = int(1080); height = int(1920) #スマホ画像
width = int(1920); height = int(1080) #PC画像
config.frame_size = [width, height]
f_voice=True
f_voice_sub=True
sw=2
class Hello(MovingCameraScene):
def construct(self):
Text.set_default(font_size=30)
Polygon.set_default(stroke_color=WHITE)
MarkupText.set_default(font_size=30)
vg_scene0 = VGroup()
ax =Axes(x_range=[-3,9,1],y_range=[-1,6,1],x_length=10,y_length=7).set_y(-1)
self.add(ax)
def other_dots(dots):
dots["A"] = np.array([max(dots["P"][0], dots["Q"][0]), 0, 0])
dots["B"] = np.array([max(dots["P"][0], dots["Q"][0]), max(dots["P"][1], dots["Q"][1]), 0])
dots["C"] = np.array([min(dots["P"][0], dots["Q"][0],0), max(dots["P"][1], dots["Q"][1]), 0])
#
dots["F"] = np.array([min(dots["P"][0], dots["Q"][0],0), min(dots["P"][1], dots["Q"][1]), 0])
if dots["Q"][0]<0:
dots["D"] = np.array([0, max(dots["P"][1], dots["Q"][1]), 0])
dots["E"] = np.array([0, min(dots["P"][1], dots["Q"][1]), 0])
dots["G"] = np.array([min(dots["P"][0], dots["Q"][0],0), 0, 0])
else:
dots["D"] = np.array([min(dots["P"][0], dots["Q"][0]), min(dots["P"][1], dots["Q"][1], 0), 0])
p = line_intersection( get_points(dots, "PF"), get_points(dots, "QD"))
dots["E"] = np.array([p[0], p[1], p[2]])
# HはQからOPに下ろした垂線の足
p = get_Foot_perpendicular(dots, "OP", "Q")
dots["H"] = np.array([p[0], p[1], p[2]])
dots["R"] = dots['P'] + dots['Q']
# 画面の中心に表示させるためにあらかじめ中心まで平行移動させておく。c2pと同じことをやってる。
for k in dots.keys():
# dots[k]=np.dot(rotation_about_z(np.deg2rad(-16)), dots[k]) #-15度回転
# dots[k]=(dots[k]-np.array([-2, 9/2, 0]))*1
dots[k] = (dots[k] + ax.c2p(0, 0)) * 1
pass
return dots
dots = {
'O': np.array([0, 0, 0]),
'P': np.array([6,2, 0]),
'Q': np.array([2,5, 0]),
}
dots = other_dots(dots)
qtxt1 = Text("なぜ△OPQの面積は",font="Yu Gothic UI", font_size=40, color=WHITE).move_to([-2,5,0])
qtxt2 = MathTex(r"\frac{1}{2}\times(ad-bc)",font_size=60).next_to(qtxt1)
qtxt3 = Text("の符号を外した値で求まるのか?",font="Yu Gothic UI", font_size=40, color=WHITE).move_to([0,4,0])
#self.add(qtxt1,qtxt2)
vg_scene0.add(qtxt1,qtxt2,qtxt3)
vg_scene0.add(Text('P(a,b)',slant=ITALIC).move_to(dots['P']+clockpoti(3,0.7))) # 4
vg_scene0.add(Text('Q(c,d)',slant=ITALIC).move_to(dots['Q']+clockpoti(12,0.5))) # 5
vg_scene0.add(Text('O',slant=ITALIC).move_to(dots['O']+clockpoti(8,0.5))) # 7
vg_scene0.add(Polygon(*get_points(dots, "OPQ"), stroke_color=WHITE)) # 3
vg_scene0.add(Polygon(*get_points(dots, "EPQ"), stroke_width=0,fill_color=RED,fill_opacity=0.3)) #8
vg_scene0.add(Polygon(*get_points(dots, "QEO"), stroke_width=0,fill_color=RED,fill_opacity=0.3)) #9
vg_scene0.add(Polygon(*get_points(dots, "PEO"), stroke_width=0,fill_color=RED,fill_opacity=0.3)) #10
self.add(vg_scene0)
self.wait(0.5)
#return
if f_voice:
talk_voiv(self, 3,'3点、O(0カンマ0,ピー,aカンマb,Q,cカンマd、を頂点とする三角形の面積は','3点、O(0,0),P(a,b),Q(c,d)を頂点とする三角形の面積は')
if f_voice:
talk_voiv(self, 3,'1/2カッコエーディーマイナスbc)の符号を外した数値で求まるとされているのだ。','1/2(ad-bc)の符号を外した数値で求まるとされているのだ。')
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'これは公式とされているのだが、なぜこれで求まるのだろうか?')
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'証明をしてみるのだ。')
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'まず三角形を覆う四角形で囲ってみる。')
vg_scene0.add(Polygon(*get_points(dots, "OABC"), stroke_color=WHITE)) # 3
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'次に、P、Qからx軸y軸に垂線を下ろしてみる。')
vg_scene0.add(Line(dots['P'],dots['F'], stroke_color=WHITE)) # 3
vg_scene0.add(Line(dots['Q'],dots['D'], stroke_color=WHITE)) # 3
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'交点に名前をつけるのだ。')
vg_scene0.add(Text('A',slant=ITALIC).move_to(dots['A']+clockpoti(4,0.5))) # 7
vg_scene0.add(Text('B',slant=ITALIC).move_to(dots['B']+clockpoti(3,0.5))) # 7
vg_scene0.add(Text('C',slant=ITALIC).move_to(dots['C']+clockpoti(9,0.5))) # 7
vg_scene0.add(Text('D',slant=ITALIC).move_to(dots['D']+clockpoti(6,0.3))) # 7
vg_scene0.add(Text('E',slant=ITALIC).move_to(dots['E']+clockpoti(2,0.5))) # 7
vg_scene0.add(Text('F',slant=ITALIC).move_to(dots['F']+clockpoti(9,0.5))) # 7
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'長さも書いておく。')
vg_scene0.add([DashedLine(*get_points(dots, "BC"),path_arc=1.4,dashed_ratio=0.3),
MarkupText('a',background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "BC") + clockpoti(12, 1.3))]) #31,32
vg_scene0.add([DashedLine(*get_points(dots, "OD"),path_arc=1.4,dashed_ratio=0.3),
MarkupText('c',background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "OD") + clockpoti(6, 0.7))]) #31,32
vg_scene0.add([DashedLine(*get_points(dots, "AB"),path_arc=1.4,dashed_ratio=0.3),
MarkupText('d',background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "AB") + clockpoti(3, 1))]) #31,32
vg_scene0.add([DashedLine(*get_points(dots, "FO"),path_arc=1.4,dashed_ratio=0.3),
MarkupText('b',background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "FO") + clockpoti(9, 0.7))]) #31,32
self.wait(1.5)
if f_voice:
talk_voiv(self, 3,'さて、公式のadとはなにか。')
self.wait(0.5)
vg_scene0.add(Polygon(*get_points(dots, "OABC"), stroke_width=10,stroke_color=PURE_RED))
# 3
if f_voice:
talk_voiv(self, 3,'adとはこの赤枠の長方形の面積を表してる。')
self.wait(0.5)
vg_scene0.add(Polygon(*get_points(dots, "ODEF"), stroke_width=10,stroke_color=PURE_BLUE))
if f_voice:
talk_voiv(self, 3,'bcとはこの青枠の長方形の面積を表してる。')
self.wait(0.5)
if f_voice:
talk_voiv(self, 3,'公式はこの赤枠から青枠の部分を引いているので、')
vg_scene0.remove(vg_scene0[28])
vg_scene0.remove(vg_scene0[27])
vg_scene0.add(Polygon(*get_points(dots, "FEDABC"), stroke_width=10,stroke_color=PURE_RED))
if f_voice:
talk_voiv(self, 3,'この赤枠で囲った面積の半分が求める三角形の面積であると主張しているのだ。')
self.wait(1.5)
if f_voice:
talk_voiv(self, 3,'この赤枠からはみ出した部分だが')
vg_scene0[8] = Polygon(*get_points(dots, "QOE"),fill_color=RED,fill_opacity=0.3)
vg_scene0[9] = Polygon(*get_points(dots, "POE"), fill_color=RED, fill_opacity=0.3)
if f_voice:
talk_voiv(self, 3,'OEに線を引いて')
self.wait(1)
vg_scene0[8] = Polygon(*get_points(dots, "QOE"),fill_color=RED,fill_opacity=0.3,stroke_color=PURE_BLUE)
if f_voice:
talk_voiv(self, 3,'この三角形は、こんなふうに移動しても面積は変わらない。')
self.play(Transform(vg_scene0[8],Polygon(*get_points(dots, "QFE"), stroke_color=WHITE,fill_color=RED,fill_opacity=0.3)), run_time=3)
if f_voice:
talk_voiv(self, 3,'こちらの三角形も頂点を移動してみる。')
self.play(Transform(vg_scene0[9],Polygon(*get_points(dots, "PDE"), stroke_color=WHITE,fill_color=RED,fill_opacity=0.3)), run_time=3)
if f_voice:
talk_voiv(self, 3,'赤枠内の3つの長方形の半分に色が付いていて、それは求める三角形の面積に等しい。')
if f_voice:
talk_voiv(self, 3,'これであの公式でちゃんと三角形の面積が求められることが証明されたのだ。')
self.wait(2)
if f_voice:
talk_voiv(self, 3, 'しかし証明されたのは、PとQが第一象限にあった場合。')
if f_voice:
talk_voiv(self, 3, 'Qが第2象限にあった場合どうなるんだろう、ということでやってみる。')
dots = {
'O': np.array([0, 0, 0]),
'P': np.array([6,2, 0]),
'Q': np.array([-2,5, 0]),
}
dots = other_dots(dots)
print(dots)
for i in range(3,len(vg_scene0)):
vg_scene0.remove(vg_scene0[3])
pass
#print([str(i) + ": " + str(x) for i, x in enumerate(vg_scene0.submobjects)]);self.wait(0.5);return
vg_scene0.add(Text('P(a,b)', slant=ITALIC).move_to(dots['P'] + clockpoti(3, 0.7))) # 4
vg_scene0.add(Text('Q(-c,d)', slant=ITALIC).move_to(dots['Q'] + clockpoti(12, 0.5))) # 5
vg_scene0.add(Text('O', slant=ITALIC).move_to(dots['O'] + clockpoti(8, 0.5))) # 7
vg_scene0.add(Polygon(*get_points(dots, "OPQ"), stroke_color=WHITE)) # 3
vg_scene0.add(Polygon(*get_points(dots, "OPE"), stroke_width=0, fill_color=RED, fill_opacity=0.3)) # 8
vg_scene0.add(Polygon(*get_points(dots, "PCE"), stroke_width=0, fill_color=RED, fill_opacity=0.3)) # 9
vg_scene0.add(Polygon(*get_points(dots, "OCE"), stroke_width=0, fill_color=RED, fill_opacity=0.3)) # 10
self.add(vg_scene0)
if f_voice:
talk_voiv(self, 3, '図に描くと、こういうパターン。')
self.wait(2)
if f_voice:
talk_voiv(self, 3, 'こちらも同じように四角形で囲んでみると、こうなる。')
vg_scene0.add(Polygon(*get_points(dots, "GABC"), stroke_color=WHITE)) # 3
vg_scene0.add(Line(dots['P'], dots['F'], stroke_color=WHITE)) # 3
vg_scene0.add(Line(dots['Q'], dots['D'], stroke_color=WHITE)) # 3
vg_scene0.add(Text('A', slant=ITALIC).move_to(dots['A'] + clockpoti(4, 0.5))) # 7
vg_scene0.add(Text('B', slant=ITALIC).move_to(dots['B'] + clockpoti(3, 0.5))) # 7
vg_scene0.add(Text('C', slant=ITALIC).move_to(dots['C'] + clockpoti(9, 0.5))) # 7
vg_scene0.add(Text('D', slant=ITALIC).move_to(dots['D'] + clockpoti(12, 0.3))) # 7
vg_scene0.add(Text('E', slant=ITALIC).move_to(dots['E'] + clockpoti(2, 0.5))) # 7
vg_scene0.add(Text('F', slant=ITALIC).move_to(dots['F'] + clockpoti(9, 0.5))) # 7
vg_scene0.add([DashedLine(*get_points(dots, "BD"), path_arc=1.4, dashed_ratio=0.3),
MarkupText('a', background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "BD") + clockpoti(12, 1.3))]) # 31,32
vg_scene0.add([DashedLine(*get_points(dots, "GO"), path_arc=1.4, dashed_ratio=0.3),
MarkupText('c', background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "GO") + clockpoti(6, 0.7))]) # 31,32
vg_scene0.add([DashedLine(*get_points(dots, "AB"), path_arc=1.4, dashed_ratio=0.3),
MarkupText('d', background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "AB") + clockpoti(3, 1))]) # 31,32
vg_scene0.add([DashedLine(*get_points(dots, "FG"), path_arc=1.4, dashed_ratio=0.3),
MarkupText('b', background_stroke_color=RED)
.move_to(get_internal_division_point(dots, "FG") + clockpoti(9, 0.7))]) # 31,32
self.wait(0.5)
vg_scene0.add(Polygon(*get_points(dots, "OABD"), stroke_width=10, stroke_color=PURE_RED))
if f_voice:
talk_voiv(self, 3, 'adというのがこの長方形の面積。')
self.wait(0.5)
if f_voice:
talk_voiv(self, 3, 'bcはこの長方形の面積。')
self.wait(0.5)
vg_scene0.add(Polygon(*get_points(dots, "GFEO"), stroke_width=10,stroke_color=PURE_BLUE))
if f_voice:
talk_voiv(self, 3, '赤い長方形が一番外側の長方形を表してるのではないが、')
if f_voice:
talk_voiv(self, 3, 'Qがx成分がマイナスcになってるから','Qがx成分が-cになってるから')
if f_voice:
talk_voiv(self, 3, 'ad-bcって、この長方形の足し算になって')
vg_scene0.remove(vg_scene0[28])
vg_scene0[27]=Polygon(*get_points(dots, "GABDEF"), stroke_width=10, stroke_color=PURE_RED)
print([str(i) + ": " + str(x) for i, x in enumerate(vg_scene0.submobjects)]);
self.wait(0.5);
if f_voice:
talk_voiv(self, 3, 'さっきと同じ鍵型の図形の面積を表すことになり、')
self.play(Transform(vg_scene0[8], Polygon(*get_points(dots, "PDE"), stroke_color=WHITE, fill_color=RED, fill_opacity=0.3)), run_time=1)
self.play(Transform(vg_scene0[9], Polygon(*get_points(dots, "OFE"), stroke_color=WHITE, fill_color=RED, fill_opacity=0.3)), run_time=1)
if f_voice:
talk_voiv(self, 3, 'このように等積変形ができるのだ。')
self.wait(1);
if f_voice:
talk_voiv(self, 3, 'ということで、あの公式はPQがどこにあっても使えて、裏技でもなんでもない。')
self.wait(1);
if f_voice:
talk_voiv(self, 3, '記述式テストでも、この絵を書いて、ad-bcはこの赤枠の面積であり、\n求める三角形の2倍の面積であると書いておけば、文句を言われることはない。')
self.wait(1);
if f_voice:
talk_voiv(self, 3, 'ということで、QEDなのだ。')
print([str(i) + ": " + str(x) for i, x in enumerate(vg_scene0.submobjects)]);self.wait(0.5);return
#スクリプト配下のmedia\nvideos\n以下を消す
target_dir = r"media\videos"
print(target_dir)
if os.path.isdir(target_dir):
shutil.rmtree(target_dir)
os.mkdir(target_dir)
#pycharmで実行させるときはこれがないとファイルが作られない
f = open('media\\videos\\serifu.txt', 'w')
f1 = open('media\\videos\\daihon.csv', 'w')
scene = Hello()
scene.render()
f.close()
f1.close()
特殊台本ファイルでYMM4定義ファイルの書き換えるhtmlファイル
YMM4のversionが上がった場合、書き換えできなくなるかもしれませんので、そのときはご自分で修正してください。
中の人はこのスクリプトをそれほど多用しないので、ほとんどメンテしない。
CSV内のセリフには改行入れないでください。
<html>
<head>
<title>Ymm4DaihonPlus</title>
<style>
textarea {
width: 100%;
height: 100px;
margin-top: 10px;
}
</style>
</head>
<body>
<H2>Ymm4DaihonPlus</H2>
<H2><marquee width="50%" bgcolor="#ffff99">ゆっくりムービーメーカー4の台本機能で読み込まれたセリフの設定を変えるぜ。台本CSVをエクセルで開いてる場合はエクセルを閉じてください。</marquee></H2>
ymmpファイル<input type="file" id="select-file1" onchange="selectFile()" accept=".ymmp" /><br>
csvファイル<input type="file" id="select-file2" onchange="selectFile()" accept=".csv" disabled /><br>
<textarea id="output1"></textarea>
<textarea id="output2"></textarea>
<textarea id="output3"></textarea>
<div id="DL_area"></div>
<script>
var ymmp = 'a'
var csv = 'b'
document.getElementById('select-file1').addEventListener('change', ymmpSelect);
function ymmpSelect(){
// イベントが発生した時の処理
selectFiles = document.querySelector("#select-file1").files
// Fileオブジェクト取得
file = selectFiles[0]
// FileReaderオブジェクト取得
reader = new FileReader()
reader.readAsText(file)
// ファイル読み込み完了時の処理
reader.onload = () => {
console.log("ymmpファイル読み込み成功")
document.querySelector("#output1").innerHTML = reader.result
}
document.querySelector("#select-file2").disabled=false
}
document.getElementById('select-file2').addEventListener('change', csvSelect);
function csvSelect(){
// イベントが発生した時の処理
// ファイル読み込みエラー時の処理
reader.onerror = () => {
console.log("ymmpファイル読み込みエラー")
}
// FileListオブジェクト取得
selectFiles = document.querySelector("#select-file2").files
// Fileオブジェクト取得
file = selectFiles[0]
// FileReaderオブジェクト取得
reader = new FileReader()
reader.readAsText(file,'Shift_JIS')
// ファイル読み込み完了時の処理
reader.onload = () => {
csv = reader.result
//document.querySelector("#output").innerHTML = csv
console.log("csvファイル読み込み成功")
console.log("csvファイル出力")
document.querySelector("#output2").innerHTML = reader.result
main()
}
// ファイル読み込みエラー時の処理
reader.onerror = () => {
console.log("csvファイル読み込みエラー")
}
}
function download_txt(file_name, data) {
const blob = new Blob([data], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = file_name;
a.href = url;
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function main(){
var ymm4data = eval("(" + document.getElementById('output1').value + ")")
pos_Serif=0
console.log("Timelines:" + ymm4data['Timelines'][0]['Items'].length)
for (let i = 0; i < ymm4data['Timelines'][0]['Items'].length; i++) {
console.log("Timelines:" + ymm4data['Timelines'][0]['Items'][i]['$type'].indexOf('VoiceItem'))
console.log("Layer:" + ymm4data['Timelines'][0]['Items'][i]['Layer'])
if ((ymm4data['Timelines'][0]['Items'][i]['$type'].indexOf('VoiceItem') >0) && (ymm4data['Timelines'][0]['Items'][i]['Layer'] == 0)){
pos_Serif=i
break;
}
}
console.log("pos_Serif:" + pos_Serif)
i = 0
skip_Frame = 0
skip_Frame_plus = 0
const csv = document.getElementById('output2').value
const csv_line = csv.split(/\n/)
csv_line.forEach((line, j) => {
// j:CSVの行数 line:j行目の内容
//console.log(j, line)
if ( line.search(/^#/) > -1) {
//コメント行
} else {
var arr_csv = this.csvSplit(line)
/*
for (var j = 0; j < arr_csv.length; j++) {
console.log(j+": "+arr_csv[j])
}
//arr_csv=line.split(',')
*/
if (arr_csv.length >= 4) {
if (arr_csv[1] !== ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Serif"]) {
console.log(i,"CSVとymmpのセリフオブジェクトの順番が一致しない")
console.log(i,"CSV",arr_csv[1] )
console.log(i,"ymmp",ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Serif"])
return
}
////////////////////////////////////////////////////////
//ymmpの変更
//セリフ表示の変更
csv_pos=3 //csv_posは1始まり
//console.log("セリフ表示:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Serif"])
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
//セリフ表示の指定がある
console.log("セリフ表示:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Serif"],"→",arr_csv[csv_pos - 1].replace(/<comma>/g,","))
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Serif"] = arr_csv[csv_pos - 1].replace(/<comma>/g,",")
}
//レイヤー
csv_pos=4 //csv_posは1始まり
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
//レイヤーの指定がある
if (isNaN(arr_csv[csv_pos - 1])) {
console.log(i,"レイヤーの指定がおかしい:",line)
return
}
console.log("レイヤー変更:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Layer"],"→",parseInt(arr_csv[csv_pos - 1]))
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Layer"] = parseInt(arr_csv[csv_pos - 1])
}
//Frame
csv_pos=5 //csv_posは1始まり
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
//Frameの指定がある
if (isNaN(arr_csv[csv_pos - 1])) {
console.log(i,"Frameの指定が数値ではない:",line)
console.log(csv_pos)
console.log(arr_csv[0])
return
}
cur_Frame = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Frame"]
obj_len = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Length"]
new_Frame = parseInt(arr_csv[csv_pos - 1])
if (arr_csv[csv_pos - 1].substr( 0, 1 )=="+") {
//console.log("部分Frame変更:",arr_csv[csv_pos - 1])
skip_Frame_plus = skip_Frame_plus + parseInt(arr_csv[csv_pos - 1])
new_Frame = skip_Frame_plus + cur_Frame
}
if (cur_Frame<new_Frame) {
//後ろにずらす
//skip_Frame = skip_Frame + ( new_Frame- cur_Frame)
skip_Frame = ( new_Frame- cur_Frame) + skip_Frame_plus
console.log("Frame変更:",i,cur_Frame,"→",new_Frame)
}
//ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"] = parseInt(arr_csv[csv_pos - 1])
}
if (skip_Frame > 0) {
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Frame"] = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Frame"] + skip_Frame
}
//再生速度
csv_pos=6 //csv_posは1始まり
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
//再生速度の指定がある
if (isNaN(arr_csv[csv_pos - 1])) {
console.log(i,"再生速度の指定がおかしい:",line)
return
}
console.log("再生速度変更:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"],"→",parseInt(arr_csv[csv_pos - 1]))
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"] = parseInt(arr_csv[csv_pos - 1])
}
//動く立ち絵の表情
csv_pos=7 //csv_posは1始まり
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
arr_effect0 = arr_csv[csv_pos - 1].split("/")
for (let k = 0; k < arr_effect0.length; ++k) {
arr_effect1 = arr_effect0[k].split(':')
if (arr_effect1.length == 2) {
//設定が正しい
if (ymm4data['Timelines'][0]["Items"][pos_Serif + i]["TachieFaceParameter"][arr_effect1[0]]) {
//TachieFaceParameterのファイル名を取ってくる
x_TachieFaceParameter = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["TachieFaceParameter"][arr_effect1[0]]
arr_TachieFaceParameter = x_TachieFaceParameter.split('\\')
cur_TachieFaceParameter = arr_TachieFaceParameter[arr_TachieFaceParameter.length - 1]
new_TachieFaceParameter = ( '00' + arr_effect1[1]).slice( -2 )+".png"
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["TachieFaceParameter"][arr_effect1[0]] = x_TachieFaceParameter.replace(cur_TachieFaceParameter,new_TachieFaceParameter)
console.log("動く立ち絵の表情変更:",i,arr_effect1[0],arr_csv[1],cur_TachieFaceParameter,"→",new_TachieFaceParameter)
} else {
console.log(i,"動く立ち絵の表情の指定がおかしい1:",arr_effect1[0],"という項目はない")
return
}
} else {
console.log(i,"動く立ち絵の表情の指定がおかしい2:",arr_effect1.length,line)
return
}
}
//動く立ち絵の表情の指定がある
//console.log("再生速度変更:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"],"→",parseInt(arr_csv[csv_pos - 1]))
//ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"] = parseInt(arr_csv[csv_pos - 1])
}
csv_pos=8 //csv_posは1始まり
if ((arr_csv.length >= csv_pos-1) && (arr_csv[csv_pos - 1] != '')) {
attr_moras = arr_csv[csv_pos - 1].split("/")
arr_length = attr_moras[0].split("-")
arr_note = attr_moras[1].split("-")
//console.log(attr_moras[0])
len_phrases = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"].length
idx_moras=0
vtime=0
for (let k = 0; k < len_phrases; ++k) {
len_moras = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"][k]["moras"].length
console.log(ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"][k]["moras"])
for (let p = 0; p < len_moras; ++p) {
vowel_length = ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"][k]["moras"][p]["vowel_length"]
if (isNaN(arr_length[idx_moras])==false) {
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"][k]["moras"][p]["vowel_length"] = Number(arr_length[idx_moras])
console.log(vowel_length + " -> "+ Number(arr_length[idx_moras]))
vtime=vtime+Number(arr_length[idx_moras])
}
if (isNaN(arr_note[idx_moras])==false) {
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Pronounce"]["AudioQuery"]["accent_phrases"][k]["moras"][p]["pitch"] = Number(arr_note[idx_moras])
}
++idx_moras
}
}
if (ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceCache"]) {
if (vtime>10) {
x_vtime="00:00:"+vtime
} else {
x_vtime="00:00:0"+vtime
}
console.log("x_vtime:"+x_vtime)
ymm4data['Timelines'][0]["Items"][pos_Serif + i]["Length"] = Math. floor(vtime * 60)
delete ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceCache"]
//console.log("del VoiceCache" + ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceCache"])
}
//動く立ち絵の表情の指定がある
//console.log("再生速度変更:",i,arr_csv[1],ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"],"→",parseInt(arr_csv[csv_pos - 1]))
//ymm4data['Timelines'][0]["Items"][pos_Serif + i]["VoiceParameter"]["Speed"] = parseInt(arr_csv[csv_pos - 1])
}
////////////////////////////////////////////////////////
}
i = i + 1
}
})
// JSONへ変換
let jsonData = JSON.stringify(ymm4data);
document.getElementById('output3').value =jsonData
x_filename0 = document.querySelector("#select-file1").value.split('\\')
new_filename = "update_" + x_filename0[x_filename0.length - 1]
document.getElementById('DL_area').innerHTML=`<a href=# onclick=\"download_txt(\'${new_filename}\',document.getElementById(\'output3\').value);\">download ${new_filename}</a>`
}
function csvSplit(line){
// https://qiita.com/hatorijobs/items/dd0c730e6faba0c84203
var c = "";
var s = new String();
var data = new Array();
var singleQuoteFlg = false;
for (var i = 0; i < line.length; i++) {
c = line.charAt(i);
if (c == "," && !singleQuoteFlg) {
data.push(s.toString());
s = "";
} else if (c == "," && singleQuoteFlg) {
s = s + c;
} else if (c == '"') {
singleQuoteFlg = !singleQuoteFlg;
} else {
s = s + c;
}
}
return data;
}
</script>
</body>
</html>
0件のコメント