Merge pull request #146 from storytold/fix/97-98-89-transcripts-without-speech
Transcripts: builds without speech-to-text say so (#97, #98, #89)
This commit is contained in:
commit
be5b34b383
|
|
@ -828,16 +828,17 @@ fn build() -> Vec<CommandSpec> {
|
|||
let item_ids: Vec<ItemId> = ids.iter().filter(|i| !image_sequences.iter().any(|q| q["item"].as_u64() == Some(**i))).map(|i| ItemId(*i)).collect();
|
||||
// Settings ▸ Media Analysis & Transcription ▸ Automatically transcribe clips
|
||||
let ma = &s.prefs.media_analysis;
|
||||
if ma.auto_transcribe
|
||||
&& ma.auto_transcribe_scope == "allImported"
|
||||
&& !ids.is_empty()
|
||||
&& (s.transcriber.is_some() || crate::transcript::speech_available())
|
||||
{
|
||||
if ma.auto_transcribe && ma.auto_transcribe_scope == "allImported" && !ids.is_empty() {
|
||||
let audio: Vec<u64> = ids.iter().copied().filter(|i| s.project.item(ItemId(*i)).is_some_and(|it| it.has_audio())).collect();
|
||||
if !audio.is_empty()
|
||||
&& let Err(e) = s.execute("transcript.generate", json!({"items": audio}))
|
||||
{
|
||||
errors.push(format!("transcription: {e}"));
|
||||
if !audio.is_empty() {
|
||||
// without speech-to-text the setting can't act: say so instead of importing
|
||||
// silently untranscribed (#89)
|
||||
let r = crate::transcript::can_transcribe(s)
|
||||
.map_err(EngineError::Other)
|
||||
.and_then(|()| s.execute("transcript.generate", json!({"items": audio})));
|
||||
if let Err(e) = r {
|
||||
errors.push(format!("transcription: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let ingest = match crate::proxies::ingest(s, &item_ids) {
|
||||
|
|
|
|||
|
|
@ -481,6 +481,35 @@ fn auto_transcribe_on_import_and_transcription_defaults() {
|
|||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// In a build without speech-to-text (the release builds), the transcript commands that can't
|
||||
/// run say so through `enabled` (#97, #98), and an import with auto-transcribe on reports why it
|
||||
/// made no transcript instead of saying nothing (#89).
|
||||
#[test]
|
||||
fn transcription_without_speech_to_text_says_so() {
|
||||
let mut s = Session::default();
|
||||
assert_eq!(s.is_enabled("transcript.generate"), crate::transcript::speech_available());
|
||||
assert_eq!(s.is_enabled("transcript.downloadModel"), cfg!(feature = "speech-download"));
|
||||
if crate::transcript::speech_available() {
|
||||
return;
|
||||
}
|
||||
let dir = tmp_dir("prefs-no-speech");
|
||||
let mov = dir.join("talk.mov");
|
||||
crate::media_test_util::make_movie(&mov, filmcraft_media::DemoScene::OceanSunset, 64, 36, 24);
|
||||
// off: an import reports nothing
|
||||
let r = s.execute("file.import", json!({"paths": [mov.to_string_lossy()]})).unwrap();
|
||||
assert_eq!(r["errors"], json!([]));
|
||||
set(&mut s, "mediaAnalysis.autoTranscribe", json!(true));
|
||||
set(&mut s, "mediaAnalysis.autoTranscribeScope", json!("allImported"));
|
||||
let r = s.execute("file.import", json!({"paths": [mov.to_string_lossy()]})).unwrap();
|
||||
let errors = r["errors"].as_array().unwrap();
|
||||
assert!(errors.iter().any(|e| e.as_str().is_some_and(|e| e.starts_with("transcription:") && e.contains("not available in this build"))), "{errors:?}");
|
||||
assert!(!s.project.transcripts.contains_key(&ItemId(r["items"][0].as_u64().unwrap())));
|
||||
// with a transcriber installed (a host's), the command is enabled again
|
||||
s.transcriber = Some(std::sync::Arc::new(filmcraft_speech::FixedTranscriber { transcript: Default::default(), id: "fixed".into() }));
|
||||
assert!(s.is_enabled("transcript.generate"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardware_decoding_setting_drives_the_decoder_switch() {
|
||||
let mut s = Session::default();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,25 @@ fn has_transcripts(s: &Session) -> std::result::Result<(), String> {
|
|||
if s.project.transcripts.is_empty() { Err("there are no transcripts".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Why speech-to-text can't run in this build (no installed transcriber, built without `whisper`).
|
||||
pub(crate) const NO_SPEECH: &str =
|
||||
"speech-to-text is not available in this build (built without the `whisper` feature); import a transcript with transcript.set instead";
|
||||
|
||||
/// `transcript.generate` can run: a host installed a transcriber or the build has speech-to-text
|
||||
/// (#97: it reported enabled and then always failed).
|
||||
pub(crate) fn can_transcribe(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.transcriber.is_some() || speech_available() { Ok(()) } else { Err(NO_SPEECH.into()) }
|
||||
}
|
||||
|
||||
/// `transcript.downloadModel` can run: built with `speech-download` (#98).
|
||||
fn can_download(_: &Session) -> std::result::Result<(), String> {
|
||||
if cfg!(feature = "speech-download") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("model downloads are not available in this build (built without the `speech-download` feature)".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The media item behind a project item (subclips resolve to their parent).
|
||||
fn media_item(s: &Session, item: ItemId) -> Option<ItemId> {
|
||||
match &s.project.item(item)?.kind {
|
||||
|
|
@ -123,9 +142,7 @@ fn transcriber(s: &Session, p: &Value) -> Result<Arc<dyn Transcriber>> {
|
|||
return Err(speech_err(SpeechError::UnknownModel(model.into())));
|
||||
}
|
||||
if !filmcraft_speech::available() {
|
||||
return Err(EngineError::Other(
|
||||
"speech-to-text is not available in this build (built without the `whisper` feature); import a transcript with transcript.set instead".into(),
|
||||
));
|
||||
return Err(EngineError::Other(NO_SPEECH.into()));
|
||||
}
|
||||
let dir = models_dir().ok_or_else(|| EngineError::Other("no data directory for speech models".into()))?;
|
||||
filmcraft_speech::load(&dir, model).map_err(speech_err)
|
||||
|
|
@ -427,7 +444,7 @@ pub fn commands() -> Vec<CommandSpec> {
|
|||
"Transcribe…",
|
||||
&["Sequence", "Transcript"],
|
||||
r#"{"items":[id]?,"model":"whisper-base"?,"language":"en|auto"?,"diarize":bool?,"maxSpeakers":n?}"#,
|
||||
always,
|
||||
can_transcribe,
|
||||
generate,
|
||||
true,
|
||||
),
|
||||
|
|
@ -444,7 +461,7 @@ pub fn commands() -> Vec<CommandSpec> {
|
|||
spec("transcript.inspect", "Inspect Transcript", &[], r#"{"paragraphGapSeconds":f?}"#, always, inspect, false),
|
||||
spec("transcript.search", "Search Transcript", &[], r#"{"query":str}"#, always, search, false),
|
||||
spec("transcript.models", "List Speech Models", &[], "{}", always, models, false),
|
||||
spec("transcript.downloadModel", "Download Speech Model", &[], r#"{"model":"whisper-base"?}"#, always, download_model, true),
|
||||
spec("transcript.downloadModel", "Download Speech Model", &[], r#"{"model":"whisper-base"?}"#, can_download, download_model, true),
|
||||
spec("transcript.select", "Mark Selected Text", &[], r#"{"from":word,"to":word?}"#, has_transcript, select, true),
|
||||
spec("transcript.extract", "Extract Selected Text", &[], r#"{"from":word,"to":word?}"#, has_transcript, |s, p| extract_or_lift(s, p, true), true),
|
||||
spec("transcript.lift", "Lift Selected Text", &[], r#"{"from":word,"to":word?}"#, has_transcript, |s, p| extract_or_lift(s, p, false), true),
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@ fn generate_without_a_transcriber() {
|
|||
let (mut s, item, _) = session();
|
||||
s.transcriber = None;
|
||||
let e = s.execute("transcript.generate", json!({"items": [item.0], "model": "nope"})).unwrap_err().to_string();
|
||||
assert!(e.contains("unknown speech model"), "{e}");
|
||||
// a build without speech-to-text says that first: the command is disabled (#97)
|
||||
let why = if filmcraft_speech::available() { "unknown speech model" } else { "not available in this build" };
|
||||
assert!(e.contains(why), "{e}");
|
||||
if !filmcraft_speech::available() {
|
||||
let e = s.execute("transcript.generate", json!({"items": [item.0]})).unwrap_err().to_string();
|
||||
assert!(e.contains("whisper") && e.contains("not available"), "{e}");
|
||||
|
|
|
|||
Loading…
Reference in New Issue