Skip to content

Commit

Permalink
Merge pull request #115 from windymelt/airframe-di
Browse files Browse the repository at this point in the history
introduce Airframe DI
  • Loading branch information
windymelt authored Apr 19, 2024
2 parents 949cb2f + 5c077dd commit e91ee67
Show file tree
Hide file tree
Showing 17 changed files with 860 additions and 813 deletions.
5 changes: 4 additions & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
version = "3.8.1"
runner.dialect = scala213
runner.dialect = scala3
trailingCommas = always

// for Airframe DI
optIn.breaksInsideChains = true
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ lazy val root = (project in file("."))
"com.mitchtalmadge" % "ascii-data" % "1.4.0",
"ch.qos.logback" % "logback-classic" % "1.4.7",
"org.typelevel" %% "log4cats-slf4j" % "2.6.0",
"org.wvlet.airframe" %% "airframe" % "24.4.0",
scalaTest % Test,
),
assembly / mainClass := Some("com.github.windymelt.zmm.Main"),
Expand Down
32 changes: 0 additions & 32 deletions src/main/scala/com/github/windymelt/zmm/ChromiumCli.scala

This file was deleted.

63 changes: 26 additions & 37 deletions src/main/scala/com/github/windymelt/zmm/Cli.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
package com.github.windymelt.zmm

import cats.effect.ExitCode
import cats.effect.IO
import cats.effect.IOApp
import domain.repository.ScreenShot

import cats.effect.kernel.Resource
import cats.effect.{ExitCode, IO, IOApp}
import cats.effect.std.Mutex
import com.github.windymelt.zmm.domain.model.Context
import com.github.windymelt.zmm.domain.model.VoiceBackendConfig
import com.github.windymelt.zmm.domain.repository.VoiceVox
import org.http4s.syntax.header
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger

import java.io.OutputStream
import scala.concurrent.duration.FiniteDuration

abstract class Cli(logLevel: String = "INFO")
extends domain.repository.FFmpegComponent
with domain.repository.VoiceVoxComponent
with domain.repository.ScreenShotComponent
with infrastructure.FFmpegComponent
with infrastructure.VoiceVoxComponent
with util.UtilComponent {
class Cli(
voiceVox: domain.repository.VoiceVox,
ffmpeg: domain.repository.FFmpeg,
screenShotBackend: ScreenShot,
screenShotResource: IO[Resource[IO, ScreenShot]],
) {

val zmmLogo = """ _________ ______ ___
|___ /| \/ || \/ |
Expand All @@ -30,23 +31,6 @@ abstract class Cli(logLevel: String = "INFO")

implicit def logger: Logger[IO] = Slf4jLogger.getLogger[IO]

val voiceVoxUri =
sys.env.get("VOICEVOX_URI") getOrElse config.getString("voicevox.apiUri")
def voiceVox: VoiceVox = new ConcreteVoiceVox(voiceVoxUri)
def ffmpeg =
new ConcreteFFmpeg(
config.getString("ffmpeg.command"),
verbosity = logLevel match {
case "DEBUG" => ConcreteFFmpeg.Verbose
case "TRACE" => ConcreteFFmpeg.Verbose
case _ => ConcreteFFmpeg.Quiet
},
) // TODO: respect construct parameter
val chromiumNoSandBox = sys.env
.get("CHROMIUM_NOSANDBOX")
.map(_ == "1")
.getOrElse(config.getBoolean("chromium.nosandbox"))

def showVoiceVoxSpeakers(): IO[Unit] = {
import io.circe.JsonObject
import com.mitchtalmadge.asciidata.table.ASCIITable
Expand Down Expand Up @@ -82,9 +66,9 @@ abstract class Cli(logLevel: String = "INFO")
_ <- logger.debug(s"generate($filePath, $outPathString)")
_ <- showLogo
_ <- logger.debug(s"pwd: ${System.getProperty("user.dir")}")
_ <- logger.debug(s"voicevox api: ${voiceVoxUri}")
_ <- logger.debug(s"voicevox api: ${voiceVox.voiceVoxUri}")
_ <- logger.debug(
s"""ffmpeg command: ${config.getString("ffmpeg.command")}""",
s"""ffmpeg command: ${ffmpeg.ffmpegCommand}""",
)
x <- content
_ <- contentSanityCheck(x)
Expand Down Expand Up @@ -134,6 +118,8 @@ abstract class Cli(logLevel: String = "INFO")
_ => ffmpeg.zipVideoWithAudio(video, audio)
}
composedVideo <- backgroundIndicator("Composing Video").surround {
import util.Util.EqForPath

// もし設定されていればビデオを合成する。BGMと同様、同じビデオであれば結合する。
val videoWithDuration: Seq[(Option[os.Path], FiniteDuration)] =
sayCtxPairs
Expand All @@ -143,7 +129,8 @@ abstract class Cli(logLevel: String = "INFO")
) -> p._2.duration.get,
)

val reductedVideoWithDuration = groupReduction(videoWithDuration)
val reductedVideoWithDuration =
util.Util.groupReduction(videoWithDuration)

// 環境によっては上書きに失敗する?ので出力ファイルが存在する場合削除する
val outputFile = os.pwd / "output_composed.mp4"
Expand All @@ -163,6 +150,8 @@ abstract class Cli(logLevel: String = "INFO")
}
}
_ <- backgroundIndicator("Applying BGM").use { _ =>
import util.Util.EqForPath

// BGMを合成する。BGMはコンテキストで割り当てる。sayCtxPairsでsayごとにコンテキストが確定するので、同じBGMであれば結合しつつ最終的なDurationを計算する。
// たとえば、BGMa 5sec BGMa 5sec BGMb 10sec であるときは、 BGMa 10sec BGMb 10secに簡約される。
val bgmWithDuration: Seq[(Option[os.Path], FiniteDuration)] =
Expand All @@ -173,7 +162,7 @@ abstract class Cli(logLevel: String = "INFO")
) -> p._2.duration.get,
)

val reductedBgmWithDuration = groupReduction(bgmWithDuration)
val reductedBgmWithDuration = util.Util.groupReduction(bgmWithDuration)

// 環境によっては上書きに失敗する?ので出力ファイルが存在する場合削除する
val outputFilePath = os.Path(outPathString)
Expand Down Expand Up @@ -285,9 +274,9 @@ abstract class Cli(logLevel: String = "INFO")
wav <- backgroundIndicator("Synthesizing wav").use { _ =>
buildWavFile(aq, ctx.spokenByCharacterId.get, voiceVox, ctx)
}
sha1Hex <- sha1HexCode(sayElem.text.getBytes())
sha1Hex <- util.Util.sha1HexCode(sayElem.text.getBytes())
path <- backgroundIndicator("Exporting .wav file").use { _ =>
writeStreamToFile(wav, s"artifacts/voice_${sha1Hex}.wav")
util.Util.writeStreamToFile(wav, s"artifacts/voice_${sha1Hex}.wav")
}
dur <- ffmpeg.getWavDuration(path.toString)
vowels <- voiceVox.getVowels(aq)
Expand All @@ -300,7 +289,7 @@ abstract class Cli(logLevel: String = "INFO")
len <- IO.pure(
ctx.silentLength.getOrElse(FiniteDuration(3, "second")),
) // 指定してないなら3秒にしているが理由はない
sha1Hex <- sha1HexCode(len.toString.getBytes)
sha1Hex <- util.Util.sha1HexCode(len.toString.getBytes)
path <- IO.pure(os.Path(s"${os.pwd}/artifacts/silence_$sha1Hex.wav"))
wav <- backgroundIndicator("Exporting silent .wav file").use { _ =>
ffmpeg.generateSilentWav(path, len)
Expand Down Expand Up @@ -412,11 +401,11 @@ abstract class Cli(logLevel: String = "INFO")
fs2.Stream[IO, Byte](s.getBytes().toSeq: _*),
)
html <- htmlIO
sha1Hex <- sha1HexCode(html.getBytes())
sha1Hex <- util.Util.sha1HexCode(html.getBytes())
htmlPath = s"./artifacts/html/${sha1Hex}.html"
htmlFile <- fileCheck(htmlPath).ifM(
IO.pure(fs2.io.file.Path(htmlPath)),
writeStreamToFile(stream, htmlPath),
util.Util.writeStreamToFile(stream, htmlPath),
)
_ <- fileCheck(s"${htmlPath}.png").ifM(
logger.debug(s"Cache HIT: ${htmlPath}.png"),
Expand Down Expand Up @@ -462,7 +451,7 @@ abstract class Cli(logLevel: String = "INFO")
}

private def buildWavFile(
aq: AudioQuery,
aq: domain.repository.AudioQuery,
character: String,
voiceVox: VoiceVox,
ctx: Context,
Expand Down
129 changes: 129 additions & 0 deletions src/main/scala/com/github/windymelt/zmm/Design.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.github.windymelt.zmm

import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.std.Mutex
import com.github.windymelt.zmm.domain.repository.{FFmpeg, ScreenShot, VoiceVox}
import com.typesafe.config.Config
import wvlet.airframe.*
import infrastructure.{ChromeScreenShot, ConcreteFFmpeg, FirefoxScreenShot}
import org.typelevel.log4cats.Logger

object Design:
def chrome(config: Config, logLevel: String = "INFO", logger: Logger[IO]) = {
val ffmpegCommand = config.getString("ffmpeg.command")
val ffmpegVerbosity = logLevel match
case "DEBUG" => ConcreteFFmpeg.Verbose
case "TRACE" => ConcreteFFmpeg.Verbose
case _ => ConcreteFFmpeg.Quiet

val voiceVoxUri =
sys.env.getOrElse("VOICEVOX_URI", config.getString("voicevox.apiUri"))

val chromiumCommand =
sys.env
.get("CHROMIUM_CMD").getOrElse(config.getString("chromium.command"))

val chromiumNoSandBox = sys.env
.get("CHROMIUM_NOSANDBOX")
.map(_ == "1")
.getOrElse(config.getBoolean("chromium.nosandbox"))

def screenShotResource: IO[Resource[IO, ScreenShot]] = {
for {
_ <- logger.debug(
s"chromium command: $chromiumCommand, chromoumNoSandBox: $chromiumNoSandBox",
)
mu <- Mutex[IO]
} yield mu.lock.map { _ =>
new infrastructure.ChromeScreenShot(
chromiumCommand,
logLevel match {
case "TRACE" => ChromeScreenShot.Verbose
case "DEBUG" => ChromeScreenShot.Verbose
case _ => ChromeScreenShot.Quiet
},
chromiumNoSandBox,
)
}
}

newDesign
.bind[FFmpeg].toInstance(
ConcreteFFmpeg("ffmpeg", ffmpegVerbosity),
)
.bind[ScreenShot].toInstance(
infrastructure.ChromeScreenShot(
chromiumCommand,
logLevel match {
case "TRACE" => ChromeScreenShot.Verbose
case "DEBUG" => ChromeScreenShot.Verbose
case _ => ChromeScreenShot.Quiet
},
chromiumNoSandBox,
),
)
.bind[IO[Resource[IO, ScreenShot]]].toInstance(screenShotResource)
.bind[VoiceVox].toInstance(
infrastructure.ConcreteVoiceVox(voiceVoxUri),
)
}

def firefox(config: Config, logLevel: String = "INFO", logger: Logger[IO]) = {
val ffmpegCommand = config.getString("ffmpeg.command")
val ffmpegVerbosity = logLevel match
case "DEBUG" => ConcreteFFmpeg.Verbose
case "TRACE" => ConcreteFFmpeg.Verbose
case _ => ConcreteFFmpeg.Quiet

val voiceVoxUri =
sys.env.getOrElse("VOICEVOX_URI", config.getString("voicevox.apiUri"))

val firefoxCommand =
sys.env.get("FIREFOX_CMD").getOrElse(config.getString("firefox.command"))

def screenShotResource: IO[Resource[IO, ScreenShot]] =
for {
_ <- logger.debug(s"firefox command: $firefoxCommand")
mx <- Mutex[IO]
} yield mx.lock.map { _ =>
new FirefoxScreenShot(
firefoxCommand,
logLevel match {
case "TRACE" => FirefoxScreenShot.Verbose
case "DEBUG" => FirefoxScreenShot.Verbose
case _ => FirefoxScreenShot.Quiet
},
)
}

newDesign
.bind[FFmpeg].toInstance(
ConcreteFFmpeg("ffmpeg", ffmpegVerbosity),
)
.bind[ScreenShot].toInstance(
new FirefoxScreenShot(
firefoxCommand,
logLevel match {
case "TRACE" => FirefoxScreenShot.Verbose
case "DEBUG" => FirefoxScreenShot.Verbose
case _ => FirefoxScreenShot.Quiet
},
),
)
.bind[IO[Resource[IO, ScreenShot]]].toInstance(screenShotResource)
.bind[VoiceVox].toInstance(
infrastructure.ConcreteVoiceVox(voiceVoxUri),
)
}

extension (d: Design)
// Because `build` is inline function, we should define `runIO` as inline function.
/** Wraps Session from Airframe DI with Resource.
*/
inline def runIO[A, B](f: A => IO[B]): IO[B] =
import scala.util.chaining.*
Resource
.make(IO(d.newSession.tap(_.start)))(s => IO(s.shutdown))
.map(_.build[A])
.use(f)
27 changes: 0 additions & 27 deletions src/main/scala/com/github/windymelt/zmm/FirefoxCli.scala

This file was deleted.

Loading

0 comments on commit e91ee67

Please sign in to comment.