track2line_lib/
lib.rs

1#[cfg(feature = "experimental")]
2mod transcription;
3
4#[cfg(feature = "config")]
5pub mod config;
6
7use std::{
8    fmt,
9    fs::{self},
10    io,
11    path::{Path, PathBuf},
12};
13
14#[derive(Debug)]
15pub enum Error {
16    IoError(io::Error),
17    ExtensionError,
18    FailedCreateRenamedFolder(io::Error),
19    NoParent,
20}
21impl fmt::Display for Error {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Error::IoError(error) => writeln!(f, "{}", error),
25            Error::ExtensionError => writeln!(f, "extension error"),
26            Error::FailedCreateRenamedFolder(e) => writeln!(f, "failed create renamed folder{}", e),
27            Error::NoParent => writeln!(f, "no parent"),
28        }
29    }
30}
31
32#[derive(Debug)]
33struct PathSet {
34    audio_path: PathBuf,
35    changed_audio_path: Option<PathBuf>,
36    line: String,
37}
38impl PathSet {
39    /// init時に変更後の`changed_audio_path`が取得できることはないため引数は以下のみ
40    fn new<P: AsRef<Path>, S: AsRef<str>>(audio_path: P, line: S) -> Self {
41        Self {
42            audio_path: audio_path.as_ref().to_path_buf(),
43            changed_audio_path: None,
44            line: line.as_ref().to_string(),
45        }
46    }
47}
48impl fmt::Display for PathSet {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        writeln!(f, "Audio: {}", self.audio_path.display())?;
51        writeln!(f, "Line: {}", self.line)?;
52        Ok(())
53    }
54}
55
56pub struct ListForCheck(Vec<(Option<String>, Option<String>)>);
57
58impl ListForCheck {
59    fn new() -> Self {
60        Self(Vec::<(Option<String>, Option<String>)>::new())
61    }
62}
63
64impl fmt::Display for ListForCheck {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        for (old, new) in &self.0 {
67            let oldd = &old
68                .as_ref()
69                .unwrap_or(&"None".to_string())
70                .chars()
71                .take(20)
72                .collect::<String>();
73
74            writeln!(
75                f,
76                "* {:width$} ---> {}",
77                oldd,
78                new.as_ref().unwrap_or(&"None".to_string()),
79                width = 20
80            )?;
81        }
82        Ok(())
83    }
84}
85
86#[derive(Debug)]
87pub struct PathSets {
88    work_dir: PathBuf,
89    list: Vec<PathSet>,
90    audio_extension: String,
91    // line_extension: String,
92}
93impl fmt::Display for PathSets {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        for i in &self.list {
96            writeln!(f, "{}", i)?;
97        }
98        Ok(())
99    }
100}
101impl PathSets {
102    /// create a new instance of PathSets.
103    /// # Arguments
104    /// * `dir` - The directory where the audio and line files are located.
105    /// * `audio_extension` - The extension of the audio file.
106    /// * `line_extension` - The extension of the line file.
107    pub fn new<P: AsRef<Path>, S: AsRef<str>>(
108        dir: P,
109        audio_extension: S,
110        line_extension: S,
111    ) -> Result<Self, Error> {
112        let filtered_path_list =
113            get_file_list(&dir, audio_extension.as_ref(), line_extension.as_ref())?;
114
115        let tmp_list = build_path_sets(
116            filtered_path_list,
117            audio_extension.as_ref(),
118            line_extension.as_ref(),
119        )?;
120
121        let mut new = PathSets {
122            work_dir: dir.as_ref().to_path_buf(),
123            list: tmp_list,
124            audio_extension: audio_extension.as_ref().to_string(),
125        };
126        new.ready_rename();
127        Ok(new)
128    }
129
130    /// この関数はまだ正常に動作しません
131    #[cfg(feature = "experimental")]
132    pub fn new_transcription<P: AsRef<Path>, S: AsRef<str>>(
133        dir: P,
134        audio_extension: S,
135        line_extension: S,
136    ) -> Result<Self, Error> {
137        let filtered_path_list =
138            get_file_list(&dir, audio_extension.as_ref(), line_extension.as_ref())?;
139
140        let path_set_list = filtered_path_list
141            .iter()
142            .filter(|f| f.extension().unwrap().to_str() == Some(audio_extension.as_ref()))
143            .map(|audio_path| {
144                let line = transcription::transcription("model_path", audio_path, Some("ja"));
145                PathSet::new(audio_path, line)
146            })
147            .collect();
148
149        let mut new = PathSets {
150            work_dir: dir.as_ref().to_path_buf(),
151            list: path_set_list,
152            audio_extension: audio_extension.as_ref().to_string(),
153        };
154        new.ready_rename();
155        Ok(new)
156    }
157
158    /// self.lineの内容を元にchanged_audio_pathをSome(path)に書き換え
159    fn ready_rename(&mut self) {
160        for i in &mut self.list {
161            //build_path_sets()にてセリフが空の処理はしてあるためここでは不要
162            i.changed_audio_path = Some(
163                self.work_dir
164                    .join("renamed")
165                    .join(&i.line)
166                    .with_extension(&self.audio_extension),
167            );
168        }
169    }
170
171    /// return list of path to be changed(not renamed yet)
172    pub fn check(&self) -> Result<ListForCheck, Error> {
173        let mut list_for_check = ListForCheck::new();
174        for i in &self.list {
175            list_for_check.0.push((
176                i.audio_path
177                    .file_name()
178                    .map(|f| f.to_string_lossy().to_string()),
179                i.changed_audio_path
180                    .as_ref()
181                    .and_then(|f| f.file_name().map(|f| f.to_string_lossy().to_string())),
182            ));
183        }
184        Ok(list_for_check)
185    }
186
187    /// rename audio files
188    pub fn rename(&mut self) -> Result<(), Error> {
189        create_renamed_folder(&self.work_dir)?;
190        for i in &mut self.list {
191            let changed_audio = match i.changed_audio_path.as_ref() {
192                Some(v) => v,
193                None => continue,
194            };
195            if fs::rename(&i.audio_path, changed_audio).is_err() {
196                i.changed_audio_path = None
197            };
198        }
199        Ok(())
200    }
201}
202
203/// Get file list
204/// audio_extentionかline_extentionにかかるファイルのみのリスト
205fn get_file_list<P: AsRef<Path>>(
206    dir: P,
207    audio_ext: &str,
208    line_ext: &str,
209) -> Result<Vec<PathBuf>, Error> {
210    Ok(fs::read_dir(&dir)
211        .map_err(Error::IoError)?
212        .filter_map(|entry| entry.ok())
213        .map(|ok_entry| ok_entry.path())
214        .filter(|entry| {
215            entry
216                .extension()
217                .is_some_and(|ext| ext == audio_ext || ext == line_ext)
218        })
219        .collect())
220}
221
222fn create_renamed_folder<P: AsRef<Path>>(dir: P) -> Result<(), Error> {
223    fs::create_dir(dir.as_ref().join("renamed")).map_err(Error::FailedCreateRenamedFolder)?;
224    Ok(())
225}
226
227/// リスト中のオーディオファイルパスから、対応するテキストファイルからセリフを20文字にカットし、Vec<Pathset>として返す
228fn build_path_sets(
229    list: Vec<PathBuf>,
230    audio_ext: &str,
231    line_ext: &str,
232) -> Result<Vec<PathSet>, Error> {
233    let mut tmp_list = Vec::<PathSet>::new();
234    let mut empty_count = 0;
235
236    for path in list {
237        if match path.extension() {
238            Some(v) => v,
239            None => continue,
240        } == audio_ext
241        {
242            // パスを探す
243            let text_path = path.with_extension(line_ext);
244
245            let mut empty = false;
246
247            let line = if text_path.exists() {
248                let tmp = fs::read_to_string(text_path)
249                    .map_err(Error::IoError)?
250                    .chars()
251                    .take(20)
252                    .collect::<String>()
253                    .trim()
254                    .to_string();
255                if tmp.is_empty() {
256                    // テキストファイルはあったが、セリフが空だった場合
257                    empty = true;
258                    format!("empty_{}", empty_count)
259                } else {
260                    // セリフがあった場合
261                    tmp
262                }
263            } else {
264                // テキストファイルがなかった場合
265                empty = true;
266                format!("empty_{}", empty_count)
267            };
268
269            if empty {
270                empty_count += 1;
271            }
272
273            let new_set = PathSet::new(path, line);
274            tmp_list.push(new_set);
275        }
276    }
277    Ok(tmp_list)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use std::{env, process};
284
285    #[test]
286    fn ready_foo() {
287        ready();
288        let a = env::current_dir().unwrap().join("assets_for_test/assets");
289        create_renamed_folder(a).unwrap();
290    }
291
292    /// run ready_test_files.ps1 or ready_test_files.sh for test ready
293    fn ready() -> bool {
294        let p = env::current_dir().unwrap().to_path_buf();
295        let windows_path = p.join("ready_test_files.ps1");
296        let unix_path = p.join("ready_test_files.sh");
297
298        if cfg!(target_os = "windows") {
299            process::Command::new("powershell")
300                .args([
301                    "-ExecutionPolicy",
302                    "Bypass",
303                    "-NoExit",
304                    "-File",
305                    windows_path.to_str().unwrap(),
306                ])
307                .status()
308                .unwrap()
309                .success()
310        } else {
311            process::Command::new("sh")
312                .args([unix_path.to_str().unwrap()])
313                .status()
314                .unwrap()
315                .success()
316        }
317    }
318
319    #[test]
320    fn ready_function() {
321        ready();
322        println!(
323            "{:?}",
324            get_file_list(
325                env::current_dir()
326                    .unwrap()
327                    .join("assets_for_test")
328                    .join("assets"),
329                "wav",
330                "txt"
331            )
332            .unwrap()
333        );
334    }
335
336    #[test]
337    fn list_assets() {
338        ready();
339        let cud = env::current_dir()
340            .unwrap()
341            .join("assets_for_test")
342            .join("assets");
343        let a = PathSets::new(&cud, "wav", "txt").unwrap();
344        for i in a.list {
345            println!("{:?}", i);
346        }
347    }
348
349    #[test]
350    fn test_check() {
351        ready();
352        let cud = env::current_dir()
353            .unwrap()
354            .join("assets_for_test")
355            .join("assets");
356        let a = PathSets::new(&cud, "wav", "txt").unwrap().check().unwrap();
357        println!("{}", a);
358    }
359
360    #[test]
361    fn test_rename() {
362        ready();
363        let cud = env::current_dir()
364            .unwrap()
365            .join("assets_for_test")
366            .join("assets");
367        PathSets::new(&cud, "wav", "txt").unwrap().rename().unwrap();
368        for i in fs::read_dir(cud.join("renamed")).unwrap() {
369            println!("{:?}", i);
370        }
371    }
372
373    #[test]
374    fn test_init_rename_prep() {
375        ready();
376        let cud = env::current_dir()
377            .unwrap()
378            .join("assets_for_test")
379            .join("assets");
380        let mut b = PathSets::new(&cud, "wav", "txt").unwrap();
381        b.ready_rename();
382        println!("{:?}", b.list);
383    }
384}