0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

動画ファイルのサイズ拡大.ps1

Posted at

1. スクリプト概要

スクリプト名: 動画ファイルのサイズ拡大.ps1
静止画像を拡大するWaifu2xCaffeを用いて動画ファイルを拡大します

2. 処理と目的

処理の流れ
 1. 対象となるファイルまたはフォルダを指定します
 2. 出力先フォルダを指定します
 3. 作業用の一時フォルダを作成します
 4. Waifu2xCaffeのオプションを指定します
 5. ffprobeで動画の情報をで取得します
 6. ffmpegで動画ファイルから音声を分離をします
 7. ffmpegで動画ファイルから画像を分離をします
 8. Waifu2xCaffeで分離した画像を拡大します
 9. 手順6で作成した音声ファイルと手順8で作成した画像ファイルを結合し、動画ファイルを作成します
 10. 手順5~9を手順1で指定したファイルの数だけ繰り返します

目的
 静止画の拡大ツールであるWaifu2xCaffeを動画に適用することに興味があったため

3. 動作環境と要件

PowerShellのバージョン
7.0以上

OS
Windows10

必要なモジュール
ffmpeg
ffprobe
Waifu2xCaffe

必要な権限
特になし

その他の設定
特になし

4. 使用方法

基本的な実行方法
スクリプトコードを拡張子ps1で保存してPowershellで実行してください。
ファイルを保存する際は、文字コードをUTF8 BOM付にしてください。
ffmpegPathffprobePathwaifu2xCaffeCuiを適切な値に変更してください

パラメータ
なし

使用例

  1. コマンドラインでpwsh 動画ファイルのサイズ拡大.ps1を実行
  2. 検索対象フォルダを尋ねられるので、フォルダのフルパスを入力します
  3. 出力先フォルダを尋ねられるので、フォルダのフルパスを入力します
  4. 変換方法を尋ねられるので、適切なパターンを選択します

5. スクリプトコード

$host.UI.RawUI.WindowTitle = ([IO.Path]::GetFilenameWithoutExtension($PSCommandPath))
$ErrorActionPreference = 'Stop'

###
# 外部ツールのパス設定
$ffmpegPath = 'D:\ffmpeg\bin\ffmpeg.exe' # ffmpeg.exeのパス
$ffprobePath = 'D:\ffmpeg\bin\ffprobe.exe' # ffprobe.exeのパス
$waifu2xCaffeCui = 'D:\Graphic\waifu2x-caffe\waifu2x-caffe-cui.exe' # waifu2x-caffe-cui.exeのパス

# 設定された外部ツールへのパスが存在するかをチェック パスが見つからない場合は警告を表示し、スクリプトを終了します
foreach($var in @($ffmpegPath, $ffprobePath, $waifu2xCaffeCui)){
    if(-not(Test-Path $var)){
      Write-Warning "NotFound ${var}"!
      pause
      return
    }
}

# 入力フォルダの指定
# ユーザーに入力フォルダのパスを尋ね、有効なパスが入力されるまで繰り返します
do {
  $inputFolder = Read-Host "入力フォルダを指定してください" # 入力ディレクトリ
  if( $inputFolder -eq ''){
    $inputFolder ='D:\tmp\INPUT'
  }
  $inputFolder = $inputFolder.Trim('"')
} while(-not (Test-Path -literal $inputFolder)) # 指定されたパスが存在するかチェック

# 入力パスがフォルダかファイルかによって処理対象のファイルタイプを設定
if ( ([IO.Directory]::Exists($inputFolder)) ){
  # 入力値がフォルダの場合:処理対象のファイル拡張子をユーザーに尋ねる
  do {
    $fileType = Read-Host "処理対象のファイル拡張子指定してください(例:*.mp4)"
    # 空の場合、デフォルトの拡張子を設定
    if( $fileType -eq ''){
      $fileType = '*.mp4' # 処理対象の拡張子
    } else {
      $fileType = $fileType
    }
  } while( $fileType -notmatch '\*\.[a-zA-Z0-9]{1,}' ) # 有効な拡張子形式かチェック
} else {
  # 入力値がファイルの場合:全てのファイルタイプを対象とする
  $fileType = '*'
}

# 出力フォルダの指定 ユーザーに出力フォルダのパスを尋ね、有効なパスが入力されるまで繰り返します
do {
  $outputFolder = Read-Host "出力フォルダを指定してください(空の場合=D:\tmp\OUTPUT)" # 出力ディレクトリ
  if( $outputFolder -eq ''){
    $outputFolder = 'D:\tmp\OUTPUT'
  }
  $outputFolder = $outputFolder.Trim('"')
} while(-not (Test-Path -literal $outputFolder)) # 指定されたパスが存在するかチェック

# 拡大サイズの定義リスト
$setsizeList = @(@{index=1; name='4K     (3840x2160)'; width=3840; height=2160},
                 @{index=2; name='2K     (2560x1440)'; width=2560; height=1440},
                 @{index=3; name='FullHD (1920x1080)'; width=1920; height=1080},
                 @{index=4; name='HD     (1280×720)'; width=1280; height=720},
                 @{index=5; name='1K     (1024×768)'; width=1024; height=768}
                )

( $setsizeList | %{$_['index'].ToString() + ":" +  $_['name']}) | Out-Default

# ユーザーに拡大サイズを選択させる 有効な選択肢が入力されるまで繰り返します
do {
  $inputText = Read-Host '拡大サイズを指定してください'
} while( $inputText -notin @($setsizeList | %{$_['index']}))

# 選択された拡大サイズに基づいて、変換後の動画の幅と高さを設定
$setsizeList | ?{$_['index'] -eq $inputText} | %{
  $movieWidth = $_['width']    # 変換後の横サイズ
  $movieHeight = $_['height']  # 変換後の縦サイズ
}

# 作業用一時フォルダの決定
# 5GB以上の空き容量があるドライブを作業用フォルダとして選択します
$tempList = (([IO.Path]::GetTempPath()), 'D:\tmp')
foreach( $folder in $tempList ){
  
  $drivename = (gi -literal $folder).PSDrive # ドライブ名を取得
  $drive = (Get-PSDrive $drivename -ErrorAction SilentlyContinue) # ドライブ情報を取得
  # 空き容量が5GB以上あるかチェック
  if( $drive.Free -gt 5gb){
    $tmpFolder = $folder # 作業用フォルダとして設定
    break
  }
}
# 適切な作業用フォルダが見つからない場合、警告を表示して終了
if( $tmpFolder -eq $null){
  Write-Warning "ドライブの空き容量が足りません。(5GB以上確保してください)"
  pause
  return
}

# Waifu2xCaffeのオプション設定
# 各種パラメータをここで定義します
# オプションの詳細はWaifu2xCaffeのGitHubリポジトリを参照
$noiseScaleMode = 'noise_scale' # ノイズ除去と拡大を同時に行うモード
$noiseLevel = 1 # ノイズ除去レベル (0-3)
$processType = 'cudnn' #-p <cpu|gpu|cudnn> # 処理方法 (CPU, GPU, cuDNN)
$maxCropSize = 512; # 最大クロップサイズ
$batchSize = 1 # バッチサイズ

# Waifu2xCaffeのモデルリスト
# 使用可能なモデルとその説明を定義します
$modelList = @(@{path='models/anime_style_art';             name='2次元イラスト (Yモデル)';       biko='画像の輝度のみを変換する2次元イラスト用モデル'}
              ,@{path='models/anime_style_art_rgb';           name='2次元イラスト(RGBモデル)';     biko='画像のRGBすべてを変換する2次元イラスト用モデル'}
              ,@{path='models/upconv_7_anime_style_art_rgb';    name='2次元イラスト(UpRGBモデル)';   biko='2次元イラスト(RGBモデル)より高速かつ同等以上の画質で変換するモデル。ただしRGBモデルより消費するメモリ(VRAM)の量が多いので、変換中に強制終了する場合は分割サイズを調節すること'}
              ,@{path='models/upresnet10';                   name='2次元イラスト (UpResNet10)';   biko='2次元イラスト(UpRGBモデル)より高画質で変換するモデル。このモデルは分割サイズが違うと出力結果が変わるので注意すること'}
              ,@{path='models/cunet';                           name='2次元イラスト (CUnet)';         biko='2次元イラストを同梱のモデルで一番高画質で変換できるモデル。このモデルは分割サイズが違うと出力結果が変わるので注意すること'}
              ,@{path='models/photo';                           name='写真・アニメ (Photoモデル)';   biko='写真・アニメ用のモデル'}
              ,@{path='models/upconv_7_photo';                 name='写真・アニメ(UpPhotoモデル)'; biko='写真・アニメ(Photoモデル)より高速かつ同等以上の画質で変換するモデル。ただしPhotoモデルより消費するメモリ(VRAM)の量が多いので、変換中に強制終了する場合は分割サイズを調節すること'}
              )

# 拡大処理に使用するモデルをGridViewで選択
$modelDir = $modelList | %{New-Object PSCustomObject -Property $_} | select 'name','path','biko' | Out-GridView -Title '拡大に使用するモデルを選択してください' -PassThru | %{$_.path}

# 一時的な画像、変換後画像、音声ファイルが保存されるパスを生成します
$tmpSourceImageFolder  = (Join-Path $tmpFolder  "\movie2x\img_s_$(Get-Date -f 'yyyyMMddHHmmssff')") # 元画像の一時保存フォルダ
$tmpSourceImageFiles   = (Join-Path $tmpSourceImageFolder  '\img_%07d.png') # 元画像ファイル名のフォーマット
$tmpConvertImageFolder = (Join-Path $tmpFolder  "\movie2x\img_c_$(Get-Date -f 'yyyyMMddHHmmssff')") # 変換後画像の一時保存フォルダ
$tmpConvertImageFiles  = (Join-Path $tmpConvertImageFolder  '\img_%07d.png') # 変換後画像ファイル名のフォーマット
$tmpSourceWaveFile      = (Join-Path $tmpFolder  "\movie2x\base_$(Get-Date -f 'yyyyMMddHHmmssff').wav") # 分離した音声ファイル

$inputFiles = ls -LiteralPath $inputFolder -Recurse -Filter $fileType

Write-Host @"
++++++++++++++++++++++++++++++++++++++++++++++++++++++
++ 入力フォルダ                = ${inputFolder}
++ 入力ファイル                = $($inputFiles -join "`n                          ")
++ 出力フォルダ                = ${outputFolder}
++ 処理対象ファイル            = ${fileType}
++ 作業用一時フォルダ          = ${tmpFolder}
++ 処理後目標サイズ            = width:${movieWidth} height:${movieHeight}
++ Waifu2xCaffe_ノイズスケール = ${noiseScaleMode}
++ Waifu2xCaffe_ノイズ除去レベル = ${noiseLevel}
++ Waifu2xCaffe_処理方法       = $processType
++ Waifu2xCaffe_最大Crobサイズ   = ${maxCropSize}
++ Waifu2xCaffe_バッチサイズ     = ${batchSize}
++ Waifu2xCaffe_拡大処理モデル   = ${modelDir}
++++++++++++++++++++++++++++++++++++++++++++++++++++++

"@

pause

##
# 主要な処理ブロック

# 外部プロセスを実行する関数
# 指定された実行ファイルと引数でプロセスを開始し、プロセスオブジェクトを返します
function RunProcess($exePathe, $argObj){
  $pinfo = New-Object System.Diagnostics.ProcessStartInfo
  $pinfo.RedirectStandardOutput = $true # 標準出力をリダイレクト
  $pinfo.UseShellExecute = $false # シェルを使用しない
  $pinfo.StandardOutputEncoding = [Text.Encoding]::UTF8 # 標準出力のエンコーディングをUTF8に設定
  ##$pinfo.StandardErrorEncoding = [Text.Encoding]::UTF8 # エラー出力のエンコーディング(コメントアウト)
  $p = New-Object System.Diagnostics.Process # 新しいプロセスオブジェクトを作成

  $pinfo.FileName = $exePathe # 実行ファイル名を設定
  $pinfo.Arguments = $argObj # 引数を設定
  $p.StartInfo = $pinfo # プロセス情報に設定
  $null = $p.Start() # プロセスを開始(出力を破棄)

  Write-Output $p # プロセスオブジェクトを返す
}

# 各入力ファイルに対する処理のループ
foreach( $file in $inputFiles ){

  # 出力ファイルが既に存在するかチェック 存在する場合は警告を表示し、次のファイルへスキップ
  if(Test-Path -literal (Join-Path $outputFolder "$($file.BaseName).mp4")){
    Write-Warning "「$($file.Name)」は既に出力ファイルが存在するので処理しません"
    continue
  }

  Write-Host "$(Get-Date) ファイル : $file の処理を開始しました。" -Foreground Black -Background White
  $host.UI.RawUI.WindowTitle = $file.Name


  ###
  # 初期化
  $cropSize = $defaultCropSize
  # 以前の一時フォルダやファイルを削除(存在する場合)
  @($tmpSourceImageFolder, $tmpConvertImageFolder, $tmpSourceWaveFile) | ?{Test-Path -literal $_} | %{
    Remove-Item $_ -recurse 2> $null # エラー出力を抑制
  }

  ###
  # ffprobeで動画情報を取得
  # 動画のフレームレート、幅、高さを取得します
  $expArg = '"' + $file.Fullname + '" -show_entries format -show_streams -print_format json -hide_banner' # ffprobe引数
  $process = RunProcess $ffprobePath $expArg # ffprobeを実行
  $null = $process.WaitForExit() # プロセスが終了するまで待機
  
  $stdout = $process.StandardOutput.ReadToEnd() # 標準出力を読み取り
  $json = $stdout | ConvertFrom-Json # JSON形式の出力をPowerShellオブジェクトに変換
  
  # 動画のフレームレート(FPS)を取得
  foreach( $propName in @('avg_frame_rate', 'r_frame_rate') ){
    $fpsString = $json.streams | ?{$_.codec_type -eq 'video'} | %{$_.$propName} # 動画ストリームからFPSを取得
    if( $fpsString -ne $null -and -not $fpsString.EndsWith('/0') ){ # 有効なFPSが見つかったらループを抜ける
      break
    }
  }
  
  # FPSが取得できない場合や60FPSを超える場合、60FPSに設定
  if( $fpsString -eq $null -or (Invoke-Expression $fpsString) -gt 60 ){
    $fpsString = '60/1'
  }
  
  # 元動画の幅と高さを取得
  $sourceMovieWidth  = $json.streams | ?{$_.codec_type -eq 'video'} | %{$_.width}
  $sourceMovieHeight = $json.streams | ?{$_.codec_type -eq 'video'} | %{$_.height}
  
  # 取得した元動画情報を表示
  echo "元動画情報 fps=${fpsString} width=${sourceMovieWidth} height=${sourceMovieHeight}"
  # 動画情報が取得できない場合、警告を表示しスキップ
  if( $sourceMovieWidth -eq $null -or $sourceMovieHeight -eq $null ){
    Write-Warning "動画情報を取得できませんでしたのでスキップします。"
    continue
  }

  # Waifu2xCaffeのクロップサイズ(分割サイズ)の候補を生成
  # maxCropSize以下の、元動画の幅の約数と、一般的なクロップサイズを組み合わせて使用
  $divWidthList = (@(512,256,128,100,64,32,16) + (($sourceMovieWidth..10) | ?{ ($sourceMovieWidth%$_) -eq 0}) ) | ?{$_ -le $maxCropSize} | sort -desc -unique
  $divWidthList = ($divWidthList + $divWidthList) | sort -desc

  ##
  # 元画像と変換後画像を保存する一時フォルダを作成します
  @($tmpSourceImageFolder, $tmpConvertImageFolder) | %{$null = New-Item $_ -type directory -Force}

  ##
  # ffmpegにより音声ファイルを分離
  Write-Host "($(Get-Date)):(1/4)音声の分離処理開始" -Foreground Cyan
  $expArg = " -i ""$($file.Fullname)"" -vn ""${tmpSourceWaveFile}"" -hide_banner" # ffmpeg引数
  $process = RunProcess $ffmpegPath $expArg # ffmpegを実行
  $process.WaitForExit() # プロセスが終了するまで待機
  # 異常終了した場合、警告を表示し一時停止
  if( $process.ExitCode -ne 0 ){
    Write-Warning "ffmpegが異常終了しました「$($process.ExitCode)」"
    pause
  }
  
  Write-Host "($(Get-Date)):(2/4)画像の分離処理開始" -Foreground Cyan
  # ffmpegにより動画を画像フレームに分離
  $argArray = @()
  $argArray += ""
  $argArray += "-i" # 入力ファイル指定
  $argArray += """$($file.Fullname)""" # 入力動画パス
  $argArray += "-hide_banner" # ffmpegのバナー非表示
  $argArray += "-r" # フレームレート設定
  $argArray += $fpsString; # 取得したFPSを使用
  $argArray += """${tmpSourceImageFiles}""" # 出力画像ファイルの命名規則
  
  $expArg = $argArray -join " " # 引数配列をスペースで結合
  
  $process = RunProcess $ffmpegPath $expArg # ffmpegを実行
  $process.WaitForExit() # プロセスが終了するまで待機
  # 異常終了した場合、警告を表示し一時停止
  if( $process.ExitCode -ne 0 ){
    Write-Warning "ffmpegが異常終了しました「$($process.ExitCode)」"
    pause
  }
  
  # Waifu2xCaffeの拡大倍率を決定
  # 元動画の解像度と目標解像度から適切な拡大倍率を設定
  $scaleString = $null
  foreach( $scale in @(16,8,4,2) ){
    if( $sourceMovieWidth -le ($movieWidth/$scale) -and $sourceMovieHeight -le ($movieHeight/$scale) ){
      $scaleString = " --scale_ratio ${scale}.0"
      break
    }
  }
  # 既に目標解像度より大きい場合や適切な倍率が見つからない場合は等倍に設定
  if( $scaleString -eq $null ){
    Write-Warning "既に解像度がでかいです width=${sourceMovieWidth} height=${sourceMovieHeight}"
    $scaleString = " --scale_ratio 1.0"
  }
  
  # 分離された画像ファイルの総数を取得
  $inCount = ([IO.Directory]::GetFiles($tmpSourceImageFolder)).Length

  ##
  # `waifu2x-caffe`による画像拡大とノイズ除去処理
  # Waifu2xCaffeの実行ディレクトリに移動
  cd -literal ([IO.Path]::GetDirectoryName($waifu2xCaffeCui))
  $expandStartTime = Get-Date # 拡大処理開始時刻を記録
  $expandSuccess = $false # 拡大処理の成功フラグ

  # クロップサイズ候補を順番に試すループ
  # メモリ不足などで失敗した場合、より小さいクロップサイズで再試行します
  foreach( $cropSize in $divWidthList ){
    # OutOfMemoryが起きないcropSizeになるまで繰り替えす
    
    # 既に変換済みの一時画像ファイルが存在する場合、元の画像ファイルを削除してディスクスペースを節約
    ls -literal $tmpConvertImageFolder | %{
      $srcimagepath = Join-Path $tmpSourceImageFolder $_.Name
      if(Test-Path -literal $srcimagepath){
        del -literal $srcimagepath # 元ファイルを削除
      }
    }
    
    # Waifu2xCaffeCuiの実行引数を構築
    $expArg = " -i ${tmpSourceImageFolder} -o ${tmpConvertImageFolder} --model_dir ${modelDir} -m ${noiseScaleMode} ${scaleString} --noise_level ${noiseLevel} -p ${processType} -c ${cropSize} -b ${batchSize}"

    Write-Host "($(Get-Date)):(3/4) waifu2x-caffeの処理開始 size=${cropSize} ${scaleString}" -Foreground Cyan
    $process = RunProcess $waifu2xCaffeCui $expArg # Waifu2xCaffeCuiを実行

    # プロセス終了まで進捗を表示しながら待機
    do{
      sleep 1 # 1秒待機
      $outCount = ([IO.Directory]::GetFiles($tmpConvertImageFolder)).Length # 変換済みファイル数を取得
      $nowTime = Get-Date # 現在時刻
      $diffTime = $nowTime - $expandStartTime # 経過時間
      $filePerSecond = $outCount / $diffTime.TotalSeconds # 1秒あたりの処理ファイル数
      
      if($filePerSecond -ne 0){
        $endTime = $nowTime.AddSeconds((($inCount-$outCount) / $filePerSecond)) # 終了予定時刻を計算
        $endDiff = $endTime - $nowTime # 残り時間
      }
      
      # 進捗バーを表示
      Write-Progress -Activity 'waifu2x-caffe' -Status "処理済みファイル: $((100*$outCount/$inCount).ToString('0.0'))% (${outCount}/${inCount}) $($filePerSecond.ToString('0.00')) file/s 残り時間:$($endDiff)$($endTime))"
    } while(-not $process.HasExited) # プロセスが終了するまで繰り返す
    Write-Progress -Activity 'waifu2x-caffe' -Status "end" -Completed # 進捗バーを完了状態にする
    
    echo $process.StandardOutput.ReadToEnd() # 標準出力を表示
    echo "終了コード = $($process.ExitCode)" # 終了コードを表示
    sleep ([Array]::IndexOf($divWidthList,$cropSize)) # 未使用?
    
    # 拡大処理が正常終了した場合、ループを抜ける
    if( $process.ExitCode -eq 0 ){
      $expandSuccess = $true
      break
    }
  }
  # 拡大処理が一度も成功しなかった場合、警告を表示し一時停止
  if($expandSuccess -eq $false){
    Write-Warning "拡大処理が正常終了しませんでした"
    pause
  }
  
  Write-Host "($(Get-Date)):(4/4)動画の作成処理開始(${inCount}files)" -Foreground Cyan
  
  # 出力用一時MP4ファイル名を生成(重複しないように)
  do {
    $tempMp4Name = "out_$(Get-Date -f 'yyyyMMdd_HHmmssff').mp4"
    $tempMp4FullName = Join-Path $outputFolder $tempMp4Name
  } while(Test-Path -literal $tempMp4FullName)
  
  ##
  # ffmpegにより画像と音声を結合してmp4動画を作成
  # H.264またはHEVC (NVENC) でエンコード
  $argList = @()
  $argList += "-r", $fpsString # フレームレート
  $argList += "-i", $tmpConvertImageFiles # 入力画像シーケンス
  if( Test-Path -literal $tmpSourceWaveFile){ # 音声ファイルが存在する場合のみ追加
    $argList += "-i", $tmpSourceWaveFile # 入力音声ファイル
  }
  $argList += "-f", "mp4" # 出力フォーマット
  $argList += "-vcodec", "hevc_nvenc" # ビデオコーデック (NVIDIA GPU用)
  $argList += "-bufsize", "20000k" # バッファサイズ
  $argList += "-maxrate", "25000k" # 最大ビットレート
  $argList += "-s", "${movieWidth}x${movieHeight}" # 出力解像度
  $argList += "-aspect", "${sourceMovieWidth}:${sourceMovieHeight}" # アスペクト比
  $argList += "-pix_fmt", "nv12" # ピクセルフォーマット
  $argList += "-hide_banner", $tempMp4FullName # ffmpegバナー非表示と出力ファイルパス
  $expArg = $argList -join " " # 引数リストをスペースで結合
  
  echo $expArg # 実行コマンドラインを表示
  
  $process = RunProcess $ffmpegPath $expArg # ffmpegを実行
  $process.WaitForExit() # プロセスが終了するまで待機

  # ffmpegが正常終了した場合の処理
  if( $process.ExitCode -eq 0 ){
    ren "${tempMp4FullName}" "$($file.BaseName).mp4" # 一時ファイル名を本来のファイル名にリネーム
    
    # 使用した一時フォルダとファイルを削除
    Remove-Item $tmpSourceImageFolder -recurse 2> $null
    Remove-Item $tmpConvertImageFolder -recurse 2> $null
    if(Test-Path -literal $tmpSourceWaveFile){
      Remove-Item $tmpSourceWaveFile 2> $null
    }
    
  } else {
    Write-Warning "ffmpegが異常終了しました「$($process.ExitCode)」"
    pause
  }
  
  Write-Host "$(Get-Date) ファイル : $file の処理が完了しました。" -Foreground Black -Background White
}

# 処理完了後、出力フォルダを開く
ls -LiteralPath $outputFolder -File | sort LastWriteTime -Descending | select -First 1 | %{explorer "/select,""$($_.fullname)"""}

$host.UI.RawUI.WindowTitle = 'fin 動画ファイルのサイズ拡大'

pause

6. 注意事項と既知の問題

制約事項
大量の画像ファイルを処理するため、処理に時間がかかる場合があります。

既知のバグ
もしバグを発見された場合は、コメントでご報告ください。

トラブルシューティング
・ps1ファイルのエンコーディングには注意してください。
ffmpeg ffprobe Waifu2xCaffeがインストールされており、スクリプト内のパスが正しく設定されているか確認してください

7. 免責事項

本スクリプトにはいかなる保証もありません。使用は自己責任で行ってください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?