Merge pull request #37 from DanielSWolf/bugfix/mutable-set

Use mutable set for animation names
This commit is contained in:
Daniel Wolf 2018-04-27 22:18:14 +02:00 committed by GitHub
commit ec698be116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 75 additions and 68 deletions

View File

@ -1,5 +1,9 @@
# Version history # Version history
## Unreleased
* Fixed bug in Rhubarb for Spine where processing failed depending on the number of existing animations. See [issue #34](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/34#issuecomment-378198776).
## Version 1.7.1 ## Version 1.7.1
* Fixed [issue #34](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/34): Generic error message in Rhubarb for Spine * Fixed [issue #34](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/34): Generic error message in Rhubarb for Spine

View File

@ -16,41 +16,41 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr
val spineJson = SpineJson(animationFilePath) val spineJson = SpineJson(animationFilePath)
val slotsProperty = SimpleObjectProperty<ObservableList<String>>() val slotsProperty = SimpleObjectProperty<ObservableList<String>>()
var slots by slotsProperty private var slots: ObservableList<String> by slotsProperty
private set
val mouthSlotProperty: SimpleStringProperty = SimpleStringProperty().alsoListen { val mouthSlotProperty: SimpleStringProperty = SimpleStringProperty().alsoListen {
mouthNaming = if (mouthSlot != null) val mouthSlot = this.mouthSlot
val mouthNaming = if (mouthSlot != null)
MouthNaming.guess(spineJson.getSlotAttachmentNames(mouthSlot)) MouthNaming.guess(spineJson.getSlotAttachmentNames(mouthSlot))
else null else null
this.mouthNaming = mouthNaming
mouthShapes = if (mouthSlot != null) { mouthShapes = if (mouthSlot != null && mouthNaming != null) {
val mouthNames = spineJson.getSlotAttachmentNames(mouthSlot) val mouthNames = spineJson.getSlotAttachmentNames(mouthSlot)
MouthShape.values().filter { mouthNames.contains(mouthNaming.getName(it)) } MouthShape.values().filter { mouthNames.contains(mouthNaming.getName(it)) }
} else listOf() } else listOf()
mouthSlotError = if (mouthSlot != null) null mouthSlotError = if (mouthSlot != null)
else "No slot with mouth drawings specified." null
else
"No slot with mouth drawings specified."
} }
var mouthSlot by mouthSlotProperty private var mouthSlot: String? by mouthSlotProperty
val mouthSlotErrorProperty = SimpleStringProperty() val mouthSlotErrorProperty = SimpleStringProperty()
var mouthSlotError by mouthSlotErrorProperty private var mouthSlotError: String? by mouthSlotErrorProperty
private set
val mouthNamingProperty = SimpleObjectProperty<MouthNaming>() val mouthNamingProperty = SimpleObjectProperty<MouthNaming>()
var mouthNaming by mouthNamingProperty private var mouthNaming: MouthNaming? by mouthNamingProperty
private set
val mouthShapesProperty = SimpleObjectProperty<List<MouthShape>>().alsoListen { val mouthShapesProperty = SimpleObjectProperty<List<MouthShape>>().alsoListen {
mouthShapesError = getMouthShapesErrorString() mouthShapesError = getMouthShapesErrorString()
} }
var mouthShapes by mouthShapesProperty var mouthShapes: List<MouthShape> by mouthShapesProperty
private set private set
val mouthShapesErrorProperty = SimpleStringProperty() val mouthShapesErrorProperty = SimpleStringProperty()
var mouthShapesError by mouthShapesErrorProperty private var mouthShapesError: String? by mouthShapesErrorProperty
private set
val audioFileModelsProperty = SimpleListProperty<AudioFileModel>( val audioFileModelsProperty = SimpleListProperty<AudioFileModel>(
spineJson.audioEvents spineJson.audioEvents
@ -63,7 +63,7 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr
} }
.observable() .observable()
) )
val audioFileModels by audioFileModelsProperty val audioFileModels: ObservableList<AudioFileModel> by audioFileModelsProperty
val busyProperty = SimpleBooleanProperty().apply { val busyProperty = SimpleBooleanProperty().apply {
bind(object : BooleanBinding() { bind(object : BooleanBinding() {
@ -90,10 +90,9 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr
} }
}) })
} }
val valid by validProperty
private fun saveAnimation(animationName: String, audioEventName: String, mouthCues: List<MouthCue>) { private fun saveAnimation(animationName: String, audioEventName: String, mouthCues: List<MouthCue>) {
spineJson.createOrUpdateAnimation(mouthCues, audioEventName, animationName, mouthSlot, mouthNaming) spineJson.createOrUpdateAnimation(mouthCues, audioEventName, animationName, mouthSlot!!, mouthNaming!!)
spineJson.save() spineJson.save()
} }

View File

@ -11,6 +11,7 @@ import javafx.scene.control.Alert
import javafx.scene.control.ButtonType import javafx.scene.control.ButtonType
import tornadofx.getValue import tornadofx.getValue
import tornadofx.setValue import tornadofx.setValue
import java.nio.file.Path
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Future import java.util.concurrent.Future
@ -20,15 +21,14 @@ class AudioFileModel(
private val executor: ExecutorService, private val executor: ExecutorService,
private val reportResult: (List<MouthCue>) -> Unit private val reportResult: (List<MouthCue>) -> Unit
) { ) {
val spineJson = parentModel.spineJson private val spineJson = parentModel.spineJson
val audioFilePath = spineJson.audioDirectoryPath.resolve(audioEvent.relativeAudioFilePath) private val audioFilePath: Path = spineJson.audioDirectoryPath.resolve(audioEvent.relativeAudioFilePath)
val eventNameProperty = SimpleStringProperty(audioEvent.name) val eventNameProperty = SimpleStringProperty(audioEvent.name)
val eventName by eventNameProperty val eventName: String by eventNameProperty
val displayFilePathProperty = SimpleStringProperty(audioEvent.relativeAudioFilePath) val displayFilePathProperty = SimpleStringProperty(audioEvent.relativeAudioFilePath)
val displayFilePath by displayFilePathProperty
val animationNameProperty = SimpleStringProperty().apply { val animationNameProperty = SimpleStringProperty().apply {
val mainModel = parentModel.parentModel val mainModel = parentModel.parentModel
@ -45,13 +45,13 @@ class AudioFileModel(
} }
}) })
} }
val animationName by animationNameProperty val animationName: String by animationNameProperty
val dialogProperty = SimpleStringProperty(audioEvent.dialog) val dialogProperty = SimpleStringProperty(audioEvent.dialog)
val dialog: String? by dialogProperty private val dialog: String? by dialogProperty
val animationProgressProperty = SimpleObjectProperty<Double?>(null) val animationProgressProperty = SimpleObjectProperty<Double?>(null)
var animationProgress by animationProgressProperty var animationProgress: Double? by animationProgressProperty
private set private set
private val animatedProperty = SimpleBooleanProperty().apply { private val animatedProperty = SimpleBooleanProperty().apply {
@ -92,7 +92,6 @@ class AudioFileModel(
} }
}) })
} }
val audioFileState by audioFileStateProperty
val busyProperty = SimpleBooleanProperty().apply { val busyProperty = SimpleBooleanProperty().apply {
bind(object : BooleanBinding() { bind(object : BooleanBinding() {
@ -120,7 +119,6 @@ class AudioFileModel(
} }
}) })
} }
val actionLabel by actionLabelProperty
fun performAction() { fun performAction() {
if (future == null) { if (future == null) {
@ -162,21 +160,21 @@ class AudioFileModel(
} }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace(System.err); e.printStackTrace(System.err)
Platform.runLater { Platform.runLater {
Alert(Alert.AlertType.ERROR).apply { Alert(Alert.AlertType.ERROR).apply {
headerText = "Error performing lip sync for event '$eventName'." headerText = "Error performing lip sync for event '$eventName'."
contentText = if (e.message.isNullOrEmpty()) contentText = if (e is EndUserException)
// Some exceptions don't have a message
"An internal error of type ${e.javaClass.name} occurred."
else
e.message e.message
else
("An internal error occurred.\n"
+ "Please report an issue, including the following information.\n"
+ getStackTrace(e))
show() show()
} }
} }
} }
} }
future = executor.submit(wrapperTask) future = executor.submit(wrapperTask)
} }

View File

@ -0,0 +1,4 @@
package com.rhubarb_lip_sync.rhubarb_for_spine
// An exception with a human-readable message that can be shown to the end user
class EndUserException(message: String): Exception(message)

View File

@ -15,38 +15,36 @@ class MainModel(private val executor: ExecutorService) {
filePathError = getExceptionMessage { filePathError = getExceptionMessage {
animationFileModel = null animationFileModel = null
if (value.isNullOrBlank()) { if (value.isNullOrBlank()) {
throw Exception("No input file specified.") throw EndUserException("No input file specified.")
} }
val path = try { val path = try {
val trimmed = value.removeSurrounding("\"") val trimmed = value.removeSurrounding("\"")
Paths.get(trimmed) Paths.get(trimmed)
} catch (e: InvalidPathException) { } catch (e: InvalidPathException) {
throw Exception("Not a valid file path.") throw EndUserException("Not a valid file path.")
} }
if (!Files.exists(path)) { if (!Files.exists(path)) {
throw Exception("File does not exist.") throw EndUserException("File does not exist.")
} }
animationFileModel = AnimationFileModel(this, path, executor) animationFileModel = AnimationFileModel(this, path, executor)
} }
} }
var filePathString by filePathStringProperty
val filePathErrorProperty = SimpleStringProperty() val filePathErrorProperty = SimpleStringProperty()
var filePathError by filePathErrorProperty private var filePathError: String? by filePathErrorProperty
private set
val animationFileModelProperty = SimpleObjectProperty<AnimationFileModel?>() val animationFileModelProperty = SimpleObjectProperty<AnimationFileModel?>()
var animationFileModel by animationFileModelProperty var animationFileModel by animationFileModelProperty
private set private set
val animationPrefixProperty = SimpleStringProperty("say_") val animationPrefixProperty = SimpleStringProperty("say_")
var animationPrefix by animationPrefixProperty var animationPrefix: String by animationPrefixProperty
val animationSuffixProperty = SimpleStringProperty("") val animationSuffixProperty = SimpleStringProperty("")
var animationSuffix by animationSuffixProperty var animationSuffix: String by animationSuffixProperty
private fun getDefaultPathString() = FX.application.parameters.raw.firstOrNull() private fun getDefaultPathString() = FX.application.parameters.raw.firstOrNull()
} }

View File

@ -2,7 +2,7 @@ package com.rhubarb_lip_sync.rhubarb_for_spine
import java.util.* import java.util.*
class MouthNaming(val prefix: String, val suffix: String, val mouthShapeCasing: MouthShapeCasing) { class MouthNaming(private val prefix: String, private val suffix: String, private val mouthShapeCasing: MouthShapeCasing) {
companion object { companion object {
fun guess(mouthNames: List<String>): MouthNaming { fun guess(mouthNames: List<String>): MouthNaming {

View File

@ -10,7 +10,7 @@ enum class MouthShape {
get() = !this.isBasic get() = !this.isBasic
companion object { companion object {
val basicShapeCount = 6 const val basicShapeCount = 6
val basicShapes = MouthShape.values().take(basicShapeCount) val basicShapes = MouthShape.values().take(basicShapeCount)

View File

@ -24,7 +24,7 @@ class RhubarbTask(
throw InterruptedException() throw InterruptedException()
} }
if (!Files.exists(audioFilePath)) { if (!Files.exists(audioFilePath)) {
throw IllegalArgumentException("File '$audioFilePath' does not exist."); throw EndUserException("File '$audioFilePath' does not exist.")
} }
val dialogFile = if (dialog != null) TemporaryTextFile(dialog) else null val dialogFile = if (dialog != null) TemporaryTextFile(dialog) else null
@ -50,7 +50,7 @@ class RhubarbTask(
return parseRhubarbResult(resultString) return parseRhubarbResult(resultString)
} }
"failure" -> { "failure" -> {
throw Exception(message.string("reason")) throw EndUserException(message.string("reason") ?: "Rhubarb failed without reason.")
} }
} }
} }
@ -58,13 +58,13 @@ class RhubarbTask(
process.destroyForcibly() process.destroyForcibly()
throw e throw e
} catch (e: EOFException) { } catch (e: EOFException) {
throw Exception("Rhubarb terminated unexpectedly.") throw EndUserException("Rhubarb terminated unexpectedly.")
} finally { } finally {
process.waitFor(); process.waitFor()
} }
}} }}
throw Exception("An unexpected error occurred.") throw EndUserException("Audio file processing terminated in an unexpected way.")
} }
private fun parseRhubarbResult(jsonString: String): List<MouthCue> { private fun parseRhubarbResult(jsonString: String): List<MouthCue> {
@ -118,7 +118,7 @@ class RhubarbTask(
} }
currentDirectory = currentDirectory.parent currentDirectory = currentDirectory.parent
} }
throw Exception("Could not find Rhubarb Lip Sync executable '$rhubarbBinName'." throw EndUserException("Could not find Rhubarb Lip Sync executable '$rhubarbBinName'."
+ " Expected to find it in '$guiBinDirectory' or any directory above.") + " Expected to find it in '$guiBinDirectory' or any directory above.")
} }

View File

@ -6,24 +6,24 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
class SpineJson(val filePath: Path) { class SpineJson(private val filePath: Path) {
val fileDirectoryPath: Path = filePath.parent private val fileDirectoryPath: Path = filePath.parent
val json: JsonObject private val json: JsonObject
private val skeleton: JsonObject private val skeleton: JsonObject
private val defaultSkin: JsonObject private val defaultSkin: JsonObject
init { init {
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
throw Exception("File '$filePath' does not exist.") throw EndUserException("File '$filePath' does not exist.")
} }
try { try {
json = Parser().parse(filePath.toString()) as JsonObject json = Parser().parse(filePath.toString()) as JsonObject
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("Wrong file format. This is not a valid JSON file.") throw EndUserException("Wrong file format. This is not a valid JSON file.")
} }
skeleton = json.obj("skeleton") ?: throw Exception("JSON file is corrupted.") skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.")
val skins = json.obj("skins") ?: throw Exception("JSON file doesn't contain skins.") val skins = json.obj("skins") ?: throw EndUserException("JSON file doesn't contain skins.")
defaultSkin = skins.obj("default") ?: throw Exception("JSON file doesn't have a default skin.") defaultSkin = skins.obj("default") ?: throw EndUserException("JSON file doesn't have a default skin.")
validateProperties() validateProperties()
} }
@ -32,14 +32,14 @@ class SpineJson(val filePath: Path) {
audioDirectoryPath audioDirectoryPath
} }
val imagesDirectoryPath: Path get() { private val imagesDirectoryPath: Path get() {
val relativeImagesDirectory = skeleton.string("images") val relativeImagesDirectory = skeleton.string("images")
?: throw Exception("JSON file is incomplete: Images path is missing." ?: throw EndUserException("JSON file is incomplete: Images path is missing."
+ "Make sure to check 'Nonessential data' when exporting.") + "Make sure to check 'Nonessential data' when exporting.")
val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory).normalize() val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory).normalize()
if (!Files.exists(imagesDirectoryPath)) { if (!Files.exists(imagesDirectoryPath)) {
throw Exception("Could not find images directory relative to the JSON file." throw EndUserException("Could not find images directory relative to the JSON file."
+ " Make sure the JSON file is in the same directory as the original Spine file.") + " Make sure the JSON file is in the same directory as the original Spine file.")
} }
@ -48,12 +48,12 @@ class SpineJson(val filePath: Path) {
val audioDirectoryPath: Path get() { val audioDirectoryPath: Path get() {
val relativeAudioDirectory = skeleton.string("audio") val relativeAudioDirectory = skeleton.string("audio")
?: throw Exception("JSON file is incomplete: Audio path is missing." ?: throw EndUserException("JSON file is incomplete: Audio path is missing."
+ "Make sure to check 'Nonessential data' when exporting.") + "Make sure to check 'Nonessential data' when exporting.")
val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory).normalize() val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory).normalize()
if (!Files.exists(audioDirectoryPath)) { if (!Files.exists(audioDirectoryPath)) {
throw Exception("Could not find audio directory relative to the JSON file." throw EndUserException("Could not find audio directory relative to the JSON file."
+ " Make sure the JSON file is in the same directory as the original Spine file.") + " Make sure the JSON file is in the same directory as the original Spine file.")
} }
@ -80,7 +80,7 @@ class SpineJson(val filePath: Path) {
val events = json.obj("events") ?: JsonObject() val events = json.obj("events") ?: JsonObject()
val result = mutableListOf<AudioEvent>() val result = mutableListOf<AudioEvent>()
for ((name, value) in events) { for ((name, value) in events) {
if (value !is JsonObject) throw Exception("Invalid event found.") if (value !is JsonObject) throw EndUserException("Invalid event found.")
val relativeAudioFilePath = value.string("audio") ?: continue val relativeAudioFilePath = value.string("audio") ?: continue
@ -96,7 +96,7 @@ class SpineJson(val filePath: Path) {
} }
val animationNames = observableSet<String>( val animationNames = observableSet<String>(
json.obj("animations")?.map{ it.key }?.toSet() ?: setOf() json.obj("animations")?.map{ it.key }?.toMutableSet() ?: mutableSetOf()
) )
fun createOrUpdateAnimation(mouthCues: List<MouthCue>, eventName: String, animationName: String, fun createOrUpdateAnimation(mouthCues: List<MouthCue>, eventName: String, animationName: String,

View File

@ -2,11 +2,10 @@ package com.rhubarb_lip_sync.rhubarb_for_spine
import javafx.application.Platform import javafx.application.Platform
import javafx.beans.property.Property import javafx.beans.property.Property
import java.util.concurrent.ExecutionException
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import java.io.PrintWriter
import java.io.StringWriter
val List<String>.commonPrefix: String get() { val List<String>.commonPrefix: String get() {
return if (isEmpty()) "" else this.reduce { result, string -> result.commonPrefixWith(string) } return if (isEmpty()) "" else this.reduce { result, string -> result.commonPrefixWith(string) }
@ -29,14 +28,13 @@ fun <TValue, TProperty : Property<TValue>> TProperty.alsoListen(listener: (TValu
fun getExceptionMessage(action: () -> Unit): String? { fun getExceptionMessage(action: () -> Unit): String? {
try { try {
action(); action()
} catch (e: Exception) { } catch (e: Exception) {
return e.message return e.message
} }
return null return null
} }
/** /**
* Invokes a Runnable on the JFX thread and waits until it's finished. * Invokes a Runnable on the JFX thread and waits until it's finished.
* Similar to SwingUtilities.invokeAndWait. * Similar to SwingUtilities.invokeAndWait.
@ -68,4 +66,10 @@ fun runAndWait(action: () -> Unit) {
throwable?.let { throw it } throwable?.let { throw it }
} }
} }
}
fun getStackTrace(e: Exception): String {
val stringWriter = StringWriter()
e.printStackTrace(PrintWriter(stringWriter))
return stringWriter.toString()
} }