Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ DEEPGRAM_API_KEY=your_deepgram_key_here
ANTHROPIC_API_KEY=your_anthropic_key_here
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=
GROQ_API_KEY=
2,358 changes: 2,358 additions & 0 deletions src-tauri/gen/schemas/windows-schema.json

Large diffs are not rendered by default.

89 changes: 82 additions & 7 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ async fn environment_status(state: tauri::State<'_, AppState>) -> Result<Environ
has_gemini_key: std::env::var("GEMINI_API_KEY").is_ok(),
has_openai_key: std::env::var("OPENAI_API_KEY").is_ok(),
has_openrouter_key: std::env::var("OPENROUTER_API_KEY").is_ok(),
has_groq_key: std::env::var("GROQ_API_KEY").is_ok(),
llm_provider,
has_local_whisper_model,
has_ollama,
has_ytdlp: media::command_exists("yt-dlp"),
})
}

Expand Down Expand Up @@ -483,6 +485,14 @@ async fn generate_candidates(
.await
.map_err(to_command_error)?
}
"groq" => {
let key = api_key
.or_else(|| std::env::var("GROQ_API_KEY").ok())
.ok_or_else(|| "Set GROQ_API_KEY or supply Groq API Key to generate candidates.".to_string())?;
llm::detect_candidates_with_groq(&normalized, &key)
.await
.map_err(to_command_error)?
}
_ => {
let key = api_key
.or_else(|| std::env::var("DEEPSEEK_API_KEY").ok())
Expand Down Expand Up @@ -678,12 +688,80 @@ pub fn run() {
set_selected_clip_count,
render_flat_clip_for_candidate,
delete_project,
rename_project
rename_project,
check_youtube_copyright,
download_youtube_video
])
.run(tauri::generate_context!())
.expect("error while running AutoShorts");
}

#[derive(serde::Serialize)]
pub struct CopyrightCheckResult {
is_safe: bool,
license: Option<String>,
}

#[tauri::command]
async fn check_youtube_copyright(url: String) -> Result<CopyrightCheckResult, String> {
let output = std::process::Command::new("yt-dlp")
.args(&["--dump-json", &url])
.output()
.map_err(|e| format!("Failed to run yt-dlp: {}", e))?;

if !output.status.success() {
return Err("Failed to fetch video metadata from YouTube.".to_string());
}

let json_str = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&json_str).map_err(|_| "Failed to parse yt-dlp output".to_string())?;

let license = parsed.get("license").and_then(|v| v.as_str()).map(|s| s.to_string());

let is_safe = if let Some(lic) = &license {
lic.to_lowercase().contains("creative commons") || lic.to_lowercase().contains("reuse allowed")
} else {
false
};

Ok(CopyrightCheckResult { is_safe, license })
}

#[tauri::command]
async fn download_youtube_video(url: String) -> Result<String, String> {
let downloads_dir = dirs::download_dir()
.ok_or_else(|| "Could not find Downloads folder".to_string())?;

let output_template = downloads_dir.join("AutoShorts_%(id)s.%(ext)s");
let output_template_str = output_template.to_string_lossy().to_string();

let output = std::process::Command::new("yt-dlp")
.args(&[
"--format", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"--merge-output-format", "mp4",
"-o", &output_template_str,
"--print", "after_move:filepath",
"--no-simulate",
&url
])
.output()
.map_err(|e| format!("Failed to run yt-dlp: {}", e))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("yt-dlp failed: {}", stderr));
}

let stdout = String::from_utf8_lossy(&output.stdout);
let filepath = stdout.lines().last().unwrap_or("").trim();

if filepath.is_empty() || !std::path::Path::new(filepath).exists() {
return Err("Could not locate downloaded file".to_string());
}

Ok(filepath.to_string())
}

fn project_dir(state: &AppState, project_id: &str) -> PathBuf {
state.data_dir.join("projects").join(project_id)
}
Expand Down Expand Up @@ -893,12 +971,9 @@ fn build_drawtext_filters(
let mut font_option = String::new();
for path in &font_paths {
if std::path::Path::new(path).exists() {
let escaped_path = path
.replace('\\', "\\\\")
.replace(':', "\\:")
.replace('\'', "\\'")
.replace(' ', "\\ ");
font_option = format!("fontfile={}:", escaped_path);
// Windows FFmpeg requires a single backslash to escape the colon even inside single quotes
let clean_path = path.replace('\\', "/").replace(':', "\\:");
font_option = format!("fontfile='{}':", clean_path);
break;
}
}
Expand Down
155 changes: 120 additions & 35 deletions src-tauri/src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ pub async fn detect_candidates_with_deepseek(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. Favor highly shareable content: concrete stories, \
strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. Return up to 10 candidates as JSON matching this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}\n\nTranscript:\n{segments}"
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let response = reqwest::Client::new()
Expand Down Expand Up @@ -111,12 +115,16 @@ pub async fn detect_candidates_with_gemini(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. Favor highly shareable content: concrete stories, \
strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. Return up to 10 candidates as JSON matching this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}\n\nTranscript:\n{segments}"
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let model = std::env::var("GEMINI_MODEL").unwrap_or_else(|_| "gemini-2.5-flash".to_string());
Expand Down Expand Up @@ -189,12 +197,16 @@ pub async fn detect_candidates_with_openai(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. Favor highly shareable content: concrete stories, \
strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. Return up to 10 candidates as JSON matching this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}\n\nTranscript:\n{segments}"
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string());
Expand Down Expand Up @@ -247,12 +259,16 @@ pub async fn detect_candidates_with_openrouter(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. Favor highly shareable content: concrete stories, \
strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. Return up to 10 candidates as JSON matching this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}\n\nTranscript:\n{segments}"
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let model =
Expand Down Expand Up @@ -302,6 +318,71 @@ Avoid rambling setup, context-dependent references, and pure filler. Return up t
parse_candidate_json(&text, min_duration)
}

pub async fn detect_candidates_with_groq(
transcript: &NormalizedTranscript,
api_key: &str,
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let model =
std::env::var("GROQ_MODEL").unwrap_or_else(|_| "llama-3.3-70b-versatile".to_string());

let response = reqwest::Client::new()
.post("https://api.groq.com/openai/v1/chat/completions")
.header("Authorization", format!("Bearer {api_key}"))
.json(&json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt,
}
],
"temperature": 0.2,
"response_format": {
"type": "json_object"
}
}))
.send()
.await
.context("calling Groq")?;

if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(anyhow!("Groq request failed ({status}): {body}"));
}

let res_body: ChatCompletionResponse = response
.json()
.await
.context("parsing Groq response")?;
let text = res_body
.choices
.first()
.map(|c| c.message.content.clone())
.ok_or_else(|| anyhow!("Groq response did not include choices content"))?;

let min_duration = if transcript.duration < 60.0 {
(transcript.duration * 0.5).max(5.0)
} else {
30.0
};
parse_candidate_json(&text, min_duration)
}

#[derive(Debug, Serialize)]
struct ClaudeMessage<'a> {
role: &'a str,
Expand All @@ -314,12 +395,16 @@ pub async fn detect_candidates_with_claude(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);
let prompt = format!(
"You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. Favor highly shareable content: concrete stories, \
strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. Return up to 10 candidates as JSON only: \
{{\"candidates\":[{{\"start\":0,\"end\":0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}\n\nTranscript:\n{segments}"
"You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection, but extract AS MANY highly viral moments as possible. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
Clips should be 30-90 seconds long, completely self-contained, cut at clean boundaries, and deliver a massive payoff (a mind-blowing fact, hilarious joke, highly controversial opinion, or deep emotional insight). \
Return up to 25 candidates as JSON matching exactly this schema: \
{{\"candidates\":[{{\"start\":0.0,\"end\":0.0,\"score\":0.0,\"hook\":\"...\",\"rationale\":\"...\"}}]}}

Transcript:
{segments}"
);

let model =
Expand Down Expand Up @@ -381,14 +466,14 @@ pub async fn detect_candidates_with_local_llm(
) -> Result<Vec<CandidateDraft>> {
let segments = compact_segments(&transcript.segments);

let system_instructions = "You are identifying the most viral moments and strongest short-form clip candidates from a long-form transcript. \
For each candidate, the clip must be self-contained, starting with an extremely engaging hook within the first 3 seconds (to capture immediate attention on social feeds), \
30-90 seconds long, and cut at clean sentence/thought boundaries. \
let system_instructions = "You are an elite, world-class social media strategist with a track record of generating viral multi-million-view Shorts, TikToks, and Reels. \
Your sole objective is to identify the ABSOLUTE BEST, most highly-engaging, and trend-setting short-form clip candidates from the provided transcript. \
Do NOT pick random or mediocre segments. Be ruthless in your selection. \
Every candidate must have an insanely strong, curiosity-inducing hook in the first 3 seconds to stop the scroll. \
CRITICAL: Each clip candidate MUST have a duration between 30 and 90 seconds (i.e. 'end' minus 'start' must be between 30.0 and 90.0). \
Do NOT return short clips of less than 30 seconds. Combine multiple adjacent sentences to build a meaningful segment of 30-90 seconds. \
Favor highly shareable content: concrete stories, strong opinions, emotional turns, surprising or counter-intuitive claims, clear payoffs, and high-energy/dramatic peaks. \
Avoid rambling setup, context-dependent references, and pure filler. \
You MUST identify and return at least 3-5 candidates (up to 10 candidates). Do not return an empty candidates list. \
You MUST identify and return at least 3-10 candidates. Do not return an empty candidates list. \
Ensure the 'start' and 'end' values correspond to actual timestamps in the transcript. Do not output 0.0 for start and end times.";

let user_content = format!("Transcript:\n{}", segments);
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ pub struct EnvironmentStatus {
pub has_gemini_key: bool,
pub has_openai_key: bool,
pub has_openrouter_key: bool,
pub has_groq_key: bool,
pub llm_provider: String,
pub has_local_whisper_model: bool,
pub has_ollama: bool,
pub has_ytdlp: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
Loading