Instructions to use autotools/ai_video_studio with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use autotools/ai_video_studio with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf autotools/ai_video_studio:Q4_K_M # Run inference directly in the terminal: llama cli -hf autotools/ai_video_studio:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf autotools/ai_video_studio:Q4_K_M # Run inference directly in the terminal: llama cli -hf autotools/ai_video_studio:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf autotools/ai_video_studio:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf autotools/ai_video_studio:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf autotools/ai_video_studio:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf autotools/ai_video_studio:Q4_K_M
Use Docker
docker model run hf.co/autotools/ai_video_studio:Q4_K_M
- LM Studio
- Jan
- Ollama
How to use autotools/ai_video_studio with Ollama:
ollama run hf.co/autotools/ai_video_studio:Q4_K_M
- Unsloth Studio
How to use autotools/ai_video_studio with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for autotools/ai_video_studio to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for autotools/ai_video_studio to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for autotools/ai_video_studio to start chatting
- Atomic Chat new
- Docker Model Runner
How to use autotools/ai_video_studio with Docker Model Runner:
docker model run hf.co/autotools/ai_video_studio:Q4_K_M
- Lemonade
How to use autotools/ai_video_studio with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull autotools/ai_video_studio:Q4_K_M
Run and chat with the model
lemonade run user.ai_video_studio-Q4_K_M
List all available models
lemonade list
ai_video_studio / runtime /omnivoice-python /omnivoice-source /omnivoice /scripts /jsonl_to_webdataset.py
| #!/usr/bin/env python3 | |
| # Copyright 2026 Xiaomi Corp. (authors: Han Zhu) | |
| # | |
| # See ../../LICENSE for clarification regarding multiple authors | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """ | |
| Pack a JSONL audio dataset into a customed WebDataset shards | |
| (paired .tar and .jsonl files). | |
| Usage: | |
| python jsonl_to_webdataset.py \ | |
| --input data.jsonl \ | |
| --output output_dir/ \ | |
| --workers 16 \ | |
| --threads 4 \ | |
| --shard-size 1000 \ | |
| --sr 24000 | |
| Input JSONL format (one JSON object per line): | |
| {"id": "utt_001", "audio_path": "/data/wavs/001.wav", "text": "hello world", ...} | |
| Required fields: "id", "audio_path", "text" | |
| All other fields are preserved in the output metadata. | |
| Output structure: | |
| output_dir/ | |
| ├── audios/ # WebDataset tar shards | |
| │ ├── shard_000000.tar | |
| │ ├── shard_000001.tar | |
| │ └── ... | |
| ├── txts/ # Per-shard JSONL metadata (with audio_duration added) | |
| │ ├── shard_000000.jsonl | |
| │ ├── shard_000001.jsonl | |
| │ └── ... | |
| ├── data.lst # Manifest: <tar_path> <jsonl_path> <sample_count> <total_duration> | |
| └── errors.jsonl # Failed samples with error details | |
| """ | |
| import argparse | |
| import io | |
| import json | |
| import logging | |
| import multiprocessing as mp | |
| import os | |
| import random | |
| from concurrent.futures import ( | |
| FIRST_COMPLETED, | |
| ProcessPoolExecutor, | |
| ThreadPoolExecutor, | |
| as_completed, | |
| wait, | |
| ) | |
| from itertools import islice | |
| from pathlib import Path | |
| import torch | |
| import torchaudio | |
| import webdataset as wds | |
| from tqdm import tqdm | |
| import soundfile as sf | |
| from omnivoice.utils.audio import load_waveform | |
| from omnivoice.utils.common import str2bool | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser( | |
| description="Pack JSONL audio dataset into WebDataset shards." | |
| ) | |
| parser.add_argument( | |
| "--input", type=str, default="data.jsonl", help="Path to input JSONL file" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| type=str, | |
| default="emilia", | |
| help="Path to output directory", | |
| ) | |
| parser.add_argument( | |
| "--workers", | |
| type=int, | |
| default=16, | |
| help="Number of worker processes (default: 16)", | |
| ) | |
| parser.add_argument( | |
| "--threads", | |
| type=int, | |
| default=4, | |
| help="Number of threads per worker process.", | |
| ) | |
| parser.add_argument( | |
| "--shard-size", | |
| type=int, | |
| default=1000, | |
| help="Number of samples per shard (default: 1000)", | |
| ) | |
| parser.add_argument( | |
| "--sr", type=int, default=24000, help="Target sample rate (default: 24000)" | |
| ) | |
| parser.add_argument( | |
| "--shuffle", | |
| type=str2bool, | |
| default=True, | |
| help="Shuffle data by default.", | |
| ) | |
| parser.add_argument( | |
| "--shuffle-seed", | |
| type=int, | |
| default=42, | |
| help="Random seed for shuffle (default: 42)", | |
| ) | |
| parser.add_argument( | |
| "--min-duration", | |
| type=float, | |
| default=None, | |
| help="Filter out samples shorter than this (seconds).", | |
| ) | |
| parser.add_argument( | |
| "--max-duration", | |
| type=float, | |
| default=None, | |
| help="Filter out samples >= this duration (seconds).", | |
| ) | |
| return parser | |
| def read_jsonl(file_path): | |
| with open(file_path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| yield json.loads(line) | |
| def chunked_reader(iterator, chunk_size): | |
| it = iter(iterator) | |
| while chunk := list(islice(it, chunk_size)): | |
| yield chunk | |
| def process_audio_item(meta, target_sr): | |
| key = meta.get("id") | |
| audio_path = meta.get("audio_path") | |
| if not key or not audio_path: | |
| return { | |
| "error": { | |
| "id": key, | |
| "audio_path": audio_path, | |
| "reason": "missing id or audio_path", | |
| } | |
| } | |
| try: | |
| if not os.path.exists(audio_path): | |
| raise FileNotFoundError(f"{audio_path} not found") | |
| waveform, sr = load_waveform(audio_path) | |
| audio_duration = waveform.shape[1] / sr | |
| meta["audio_duration"] = audio_duration | |
| if target_sr and sr != target_sr: | |
| waveform = torchaudio.functional.resample( | |
| torch.from_numpy(waveform), orig_freq=sr, new_freq=target_sr | |
| ).numpy() | |
| sr = target_sr | |
| audio_buffer = io.BytesIO() | |
| sf.write(audio_buffer, waveform.T, sr, format="FLAC") | |
| audio_bytes = audio_buffer.getvalue() | |
| sample = { | |
| "__key__": key, | |
| "flac": audio_bytes, | |
| } | |
| return {"ok": (sample, meta)} | |
| except Exception as e: | |
| return {"error": {"id": key, "audio_path": audio_path, "reason": str(e)}} | |
| def process_single_shard( | |
| shard_idx, | |
| records, | |
| output_tar_pattern, | |
| output_jsonl_pattern, | |
| target_sr, | |
| num_threads=4, | |
| min_duration=None, | |
| max_duration=None, | |
| ): | |
| tar_fname = output_tar_pattern % shard_idx | |
| jsonl_fname = output_jsonl_pattern % shard_idx | |
| processed_count = 0 | |
| filtered_count = 0 | |
| error_count = 0 | |
| total_duration = 0.0 | |
| errors = [] | |
| with wds.TarWriter(tar_fname) as sink, open( | |
| jsonl_fname, "w", encoding="utf-8" | |
| ) as jsonl_f: | |
| with ThreadPoolExecutor(max_workers=num_threads) as thread_pool: | |
| futures = [] | |
| for meta in records: | |
| f = thread_pool.submit(process_audio_item, meta, target_sr) | |
| futures.append(f) | |
| for f in as_completed(futures): | |
| result = f.result() | |
| if "error" in result: | |
| error_count += 1 | |
| errors.append(result["error"]) | |
| continue | |
| sample, meta = result["ok"] | |
| dur = meta.get("audio_duration", 0.0) | |
| # Duration filtering (based on actual audio_duration computed above) | |
| if min_duration is not None and dur < min_duration: | |
| filtered_count += 1 | |
| continue | |
| if max_duration is not None and dur >= max_duration: | |
| filtered_count += 1 | |
| continue | |
| sink.write(sample) | |
| jsonl_f.write(json.dumps(meta, ensure_ascii=False) + "\n") | |
| total_duration += dur | |
| processed_count += 1 | |
| # Clean up empty shard files | |
| if processed_count == 0: | |
| for p in (tar_fname, jsonl_fname): | |
| if os.path.exists(p): | |
| os.remove(p) | |
| return ( | |
| shard_idx, | |
| processed_count, | |
| error_count, | |
| filtered_count, | |
| total_duration, | |
| errors, | |
| ) | |
| def count_lines(path): | |
| with open(path, "rb") as f: | |
| return sum(buf.count(b"\n") for buf in iter(lambda: f.read(1 << 20), b"")) | |
| def pack_dataset( | |
| input_jsonl, | |
| output_dir, | |
| samples_per_shard=5000, | |
| num_workers=16, | |
| target_sr=24000, | |
| threads_per_worker=4, | |
| shuffle=False, | |
| shuffle_seed=None, | |
| min_duration=None, | |
| max_duration=None, | |
| ): | |
| input_path = Path(input_jsonl) | |
| output_dir = Path(output_dir) | |
| output_tar_dir = output_dir / "audios" | |
| output_tar_dir.mkdir(parents=True, exist_ok=True) | |
| output_jsonl_dir = output_dir / "txts" | |
| output_jsonl_dir.mkdir(parents=True, exist_ok=True) | |
| output_tar_pattern = str(output_tar_dir / "shard-%06d.tar") | |
| output_jsonl_pattern = str(output_jsonl_dir / "shard-%06d.jsonl") | |
| error_log_path = str(output_dir / "errors.jsonl") | |
| # Setup error logger | |
| error_logger = logging.getLogger("error_log") | |
| error_logger.setLevel(logging.ERROR) | |
| error_logger.handlers.clear() | |
| fh = logging.FileHandler(error_log_path, mode="w", encoding="utf-8") | |
| fh.setFormatter(logging.Formatter("%(message)s")) | |
| error_logger.addHandler(fh) | |
| shard_manifest = {} | |
| print(f"Reading input: {input_path}") | |
| print(f"Output dir: {output_dir}") | |
| print(f"Strategy: {num_workers} Processes x {threads_per_worker} Threads") | |
| if shuffle: | |
| print("Load input dataset...") | |
| entries = list(read_jsonl(input_path)) | |
| random.seed(shuffle_seed) | |
| random.shuffle(entries) | |
| print(f"Shuffled {len(entries)} entries (seed={shuffle_seed})") | |
| total_lines = len(entries) | |
| chunk_gen = chunked_reader(iter(entries), samples_per_shard) | |
| else: | |
| print("Calculating total lines...") | |
| total_lines = count_lines(input_path) | |
| chunk_gen = chunked_reader(read_jsonl(input_path), samples_per_shard) | |
| if min_duration is not None or max_duration is not None: | |
| print( | |
| f"Duration filter: [{min_duration or 0:.2f}s" | |
| f", {max_duration or float('inf'):.1f}s) (applied after audio decoding)" | |
| ) | |
| total_shards_est = (total_lines + samples_per_shard - 1) // samples_per_shard | |
| print(f"Total samples: {total_lines}, Estimated shards: {total_shards_est}") | |
| with ProcessPoolExecutor(max_workers=num_workers) as executor: | |
| futures = set() | |
| shard_idx = 0 | |
| total_processed = 0 | |
| total_errors = 0 | |
| total_filtered = 0 | |
| pbar = tqdm( | |
| total=total_shards_est, | |
| desc="Shards Processed", | |
| unit="shard", | |
| ) | |
| def submit_next_chunks(limit): | |
| """Pull up to `limit` chunks from generator, submit them.""" | |
| nonlocal shard_idx | |
| submitted = 0 | |
| for chunk in chunk_gen: | |
| f = executor.submit( | |
| process_single_shard, | |
| shard_idx, | |
| chunk, | |
| output_tar_pattern, | |
| output_jsonl_pattern, | |
| target_sr, | |
| threads_per_worker, | |
| min_duration, | |
| max_duration, | |
| ) | |
| futures.add(f) | |
| shard_idx += 1 | |
| submitted += 1 | |
| if submitted >= limit: | |
| break | |
| submit_next_chunks(num_workers * 2) | |
| while futures: | |
| done, _ = wait(futures, return_when=FIRST_COMPLETED) | |
| for f in done: | |
| futures.remove(f) | |
| try: | |
| s_idx, p_count, e_count, f_count, s_duration, errors = f.result() | |
| total_processed += p_count | |
| total_errors += e_count | |
| total_filtered += f_count | |
| # Write error log | |
| for err in errors: | |
| err["shard_idx"] = s_idx | |
| error_logger.error(json.dumps(err, ensure_ascii=False)) | |
| if p_count > 0: | |
| tar_abs = os.path.abspath(output_tar_pattern % s_idx) | |
| jsonl_abs = os.path.abspath(output_jsonl_pattern % s_idx) | |
| shard_manifest[s_idx] = ( | |
| tar_abs, | |
| jsonl_abs, | |
| p_count, | |
| s_duration, | |
| ) | |
| pbar.set_postfix( | |
| { | |
| "Samples": total_processed, | |
| "Filtered": total_filtered, | |
| "Errors": total_errors, | |
| } | |
| ) | |
| pbar.update(1) | |
| except Exception as e: | |
| print(f"Shard task failed: {e}") | |
| submit_next_chunks(1) | |
| pbar.close() | |
| # Write final manifest file (data.lst) | |
| manifest_path = str(output_dir / "data.lst") | |
| with open(manifest_path, "w", encoding="utf-8") as mf: | |
| for idx in sorted(shard_manifest.keys()): | |
| tar_path, jsonl_path, count, duration = shard_manifest[idx] | |
| mf.write(f"{tar_path} {jsonl_path} {count} {duration:.3f}\n") | |
| print(f"\nDone! Output saved to {output_dir}") | |
| print(f"Successfully packed: {total_processed}") | |
| print(f"Filtered by duration: {total_filtered}") | |
| print(f"Failed: {total_errors}") | |
| print(f"Manifest written to: {manifest_path} ({len(shard_manifest)} shards)") | |
| if total_errors > 0: | |
| print(f"Error details: {error_log_path}") | |
| if __name__ == "__main__": | |
| mp.set_start_method("spawn", force=True) | |
| args = build_parser().parse_args() | |
| pack_dataset( | |
| input_jsonl=args.input, | |
| output_dir=args.output, | |
| samples_per_shard=args.shard_size, | |
| num_workers=args.workers, | |
| target_sr=args.sr, | |
| threads_per_worker=args.threads, | |
| shuffle=args.shuffle, | |
| shuffle_seed=args.shuffle_seed, | |
| min_duration=args.min_duration, | |
| max_duration=args.max_duration, | |
| ) | |