Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ jobs:
with:
python-version: '3.11'

- name: Install Application Dependencies
run: python -m pip install -r requirements.txt

- name: Run Offline Verification Suite
run: python -m unittest discover -s tests -v

- name: Validate Python Syntax
run: |
python -m compileall -q .
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ python app.py
# Or double-click "Start Video Studio.bat" on Windows
```

## 🧪 Verification

Run the offline verification suite with:

```bash
python -m unittest discover -s tests -v
```

`test_gemini_features.py` and `test_verify.py` are opt-in live smoke checks; they may use network services, API quota, and local media processing, so they are not run during automated test discovery.

---

## 📜 License
Expand Down
6 changes: 3 additions & 3 deletions Start Video Studio.bat
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
@echo off
title VideoStudio Pro - 4K AI Video ^& Storyboard Studio
cd /d "C:\Users\chkam\OneDrive\Desktop\BrandFinder\VideoStudio"
cd /d "%~dp0"

echo ================================================================
echo VideoStudio Pro - 4K AI Video ^& Storyboard Studio
echo Crafted by Kamran Ashraf (Kami)
echo ================================================================

rem --- pick a real Python (no PATH guesswork) ---
set "PY=C:\Users\chkam\AppData\Local\Programs\Python\Python314\python.exe"
rem --- prefer the project's virtual environment, then use system Python ---
set "PY=%~dp0.venv\Scripts\python.exe"
if not exist "%PY%" set "PY=python"

rem --- already running? just open the browser and exit ---
Expand Down
115 changes: 68 additions & 47 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import html
import os
import subprocess
import sys
Expand Down Expand Up @@ -534,10 +535,41 @@ def duration_hint(sec) -> str:
return (f"<span style='color:#38BDF8;font-size:0.82rem;'>🎞️ {int(float(sec))}s 4K · "
f"{n} AI shots chained & crossfaded</span>")

def job_progress_percent(progress: float) -> int:
try:
return max(0, min(100, int(float(progress) * 100)))
except (TypeError, ValueError, OverflowError):
return 0

def render_job_card(job_id: str, prompt: str, stage: Stage, progress: float, message: str, elapsed: Optional[int] = None) -> str:
pct = job_progress_percent(progress)
icon = STAGE_ICONS.get(stage, "•")
color = "#10B981" if stage == Stage.DONE else "#EF4444" if stage in (Stage.FAILED, Stage.CANCELLED) else "#38BDF8"
elapsed_html = f"<span style='float:right; opacity:0.7;'>⏱️ {max(0, elapsed)}s</span>" if elapsed is not None else ""
safe_job_id = html.escape(str(job_id))
safe_prompt = html.escape(str(prompt)[:40])
safe_stage = html.escape(stage.value)
safe_message = html.escape(str(message))
return f"""
<div class="q-card">
<div class="q-head">
<span class="q-prompt"><b>[{safe_job_id}]</b> {safe_prompt}</span>
<span class="q-stage" style="color:{color}; font-weight:800;">{icon} {safe_stage} ({pct}%)</span>
</div>
<div class="q-bar"><div class="q-fill" style="width:{pct}%;"></div></div>
<div style="margin-top:6px; font-size:0.84rem; color:#CBD5E1;">
{safe_message} {elapsed_html}
</div>
</div>
"""

def generate_ai_video_live(
prompt, negative, preset, quality, aspect, duration, language, gender,
custom_script, music, burn_subs, sub_style, tr_lang, interp_60, anti_fp, watermark, seed
):
if not STUDIO.queue.is_worker:
yield "⚠️ Another VideoStudio instance owns the job queue; this instance is view-only.", "<div class='q-card'>Queue is read-only in this instance.</div>", None, None, ""
return
if not prompt.strip():
yield "⚠️ Please enter a prompt first.", "<div class='q-card'><b>Enter a prompt</b> to generate 4K video.</div>", None, None, ""
return
Expand Down Expand Up @@ -571,23 +603,16 @@ def generate_ai_video_live(
time.sleep(1.0)
continue

pct = int(status.progress * 100)
icon = STAGE_ICONS.get(status.stage, "•")
color = "#10B981" if status.stage == Stage.DONE else "#EF4444" if status.stage in (Stage.FAILED, Stage.CANCELLED) else "#38BDF8"
pct = job_progress_percent(status.progress)
elapsed = int(time.time() - start_t)

card_html = f"""
<div class="q-card">
<div class="q-head">
<span class="q-prompt"><b>[{job_id}]</b> {status.settings.prompt[:40] if status.settings else 'Job'}</span>
<span class="q-stage" style="color:{color}; font-weight:800;">{icon} {status.stage.value} ({pct}%)</span>
</div>
<div class="q-bar"><div class="q-fill" style="width:{pct}%;"></div></div>
<div style="margin-top:6px; font-size:0.84rem; color:#CBD5E1;">
{status.message} <span style="float:right; opacity:0.7;">⏱️ {elapsed}s</span>
</div>
</div>
"""
card_html = render_job_card(
job_id,
status.settings.prompt if status.settings else "Job",
status.stage,
status.progress,
status.message,
elapsed,
)

if status.stage == Stage.DONE:
v_out = status.video_path if (status.video_path and Path(status.video_path).exists()) else None
Expand All @@ -598,7 +623,7 @@ def generate_ai_video_live(
yield f"🛑 Job Cancelled: {job_id}", card_html, None, None, job_id
break
elif status.stage == Stage.FAILED:
yield f"❌ Job Error: {status.error or 'Failed'}", card_html, None, None, job_id
yield f"❌ Job Error: {html.escape(str(status.error or 'Failed'))}", card_html, None, None, job_id
break

yield f"⏳ Rendering 4K ({pct}%)...", card_html, None, None, job_id
Expand All @@ -607,6 +632,9 @@ def generate_ai_video_live(
def generate_storyboard_video_live(
topic, script_text, preset, aspect, language, gender, music, burn_subs, sub_style, watermark, anti_fp
):
if not STUDIO.queue.is_worker:
yield "⚠️ Another VideoStudio instance owns the job queue; this instance is view-only.", "<div class='q-card'>Queue is read-only in this instance.</div>", None, ""
return
if not script_text.strip() and not topic.strip():
yield "⚠️ Please enter a topic or script.", "<div class='q-card'><b>Enter a script</b> to build video.</div>", None, ""
return
Expand Down Expand Up @@ -634,23 +662,16 @@ def generate_storyboard_video_live(
time.sleep(1.0)
continue

pct = int(status.progress * 100)
icon = STAGE_ICONS.get(status.stage, "•")
color = "#10B981" if status.stage == Stage.DONE else "#EF4444" if status.stage in (Stage.FAILED, Stage.CANCELLED) else "#38BDF8"
pct = job_progress_percent(status.progress)
elapsed = int(time.time() - start_t)

card_html = f"""
<div class="q-card">
<div class="q-head">
<span class="q-prompt"><b>[{job_id}]</b> {status.settings.prompt[:40] if status.settings else 'Job'}</span>
<span class="q-stage" style="color:{color}; font-weight:800;">{icon} {status.stage.value} ({pct}%)</span>
</div>
<div class="q-bar"><div class="q-fill" style="width:{pct}%;"></div></div>
<div style="margin-top:6px; font-size:0.84rem; color:#CBD5E1;">
{status.message} <span style="float:right; opacity:0.7;">⏱️ {elapsed}s</span>
</div>
</div>
"""
card_html = render_job_card(
job_id,
status.settings.prompt if status.settings else "Job",
status.stage,
status.progress,
status.message,
elapsed,
)

if status.stage == Stage.DONE:
v_out = status.video_path if (status.video_path and Path(status.video_path).exists()) else None
Expand All @@ -660,19 +681,23 @@ def generate_storyboard_video_live(
yield f"🛑 Storyboard Cancelled: {job_id}", card_html, None, job_id
break
elif status.stage == Stage.FAILED:
yield f"❌ Storyboard Error: {status.error or 'Failed'}", card_html, None, job_id
yield f"❌ Storyboard Error: {html.escape(str(status.error or 'Failed'))}", card_html, None, job_id
break

yield f"⏳ Building Storyboard ({pct}%)...", card_html, None, job_id
time.sleep(1.0)

def cancel_active_job_btn(job_id: str) -> str:
if not STUDIO.queue.is_worker:
return "⚠️ Another VideoStudio instance owns the job queue; this instance is view-only."
if not job_id:
return "⚠️ No active job selected."
success = STUDIO.queue.cancel(job_id)
return f"🛑 Job [{job_id}] stopped." if success else f"Job [{job_id}] not found."

def resume_all_interrupted_jobs_btn() -> str:
if not STUDIO.queue.is_worker:
return "⚠️ Another VideoStudio instance owns the job queue; this instance is view-only."
STUDIO.queue._auto_resume_interrupted_jobs()
return "🔄 Interrupted jobs restored & resumed!"

Expand Down Expand Up @@ -725,19 +750,15 @@ def render_queue_table() -> str:
return "<div class='q-card' style='font-size:0.86rem; color:#94A3B8;'>No jobs in queue. Submit a 4K Video or Storyboard to begin.</div>"
cards = []
for js in reversed(jobs[-8:]):
pct = int(js.progress * 100)
icon = STAGE_ICONS.get(js.stage, "•")
color = "#10B981" if js.stage == Stage.DONE else "#EF4444" if js.stage == Stage.CANCELLED else "#F59E0B" if js.stage == Stage.FAILED else "#38BDF8"
cards.append(f"""
<div class="q-card">
<div class="q-head">
<span class="q-prompt"><b>[{js.job_id}]</b> {js.settings.prompt[:40] if js.settings else 'Job'}</span>
<span class="q-stage" style="color:{color};">{icon} {js.stage.value} ({pct}%)</span>
</div>
<div class="q-bar"><div class="q-fill" style="width:{pct}%;"></div></div>
<div style="margin-top:6px; font-size:0.80rem; color:#94A3B8;">{js.message}</div>
</div>
""")
cards.append(
render_job_card(
js.job_id,
js.settings.prompt if js.settings else "Job",
js.stage,
js.progress,
js.message,
)
)
return "".join(cards)

# ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions assets/test_veo3_free.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
warnings.filterwarnings("ignore", message=".*numpy.*")
warnings.filterwarnings("ignore", category=UserWarning)

from video_generator import Veo3FreeVideoEngine, FreeLLMPromptEnhancer

def main():
from video_generator import Veo3FreeVideoEngine, FreeLLMPromptEnhancer

parser = argparse.ArgumentParser(description="Free VEO 3 AI Video Generator Test")
parser.add_argument("prompt", type=str, nargs="?", default="A glowing futuristic starship sailing through a purple nebula, 8k cinematic masterpiece", help="Text prompt for video generation")
parser.add_argument("--aspect", type=str, default="16:9", choices=["16:9", "9:16", "1:1"], help="Aspect Ratio")
Expand Down
18 changes: 5 additions & 13 deletions run_videostudio.vbs
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
' VideoStudio Pro — Resilient Background Launcher
Option Explicit

Dim WshShell, fso, q, appDir, py, logPath, cmdLine, i, candidates, cand
Dim WshShell, fso, q, appDir, py, logPath, cmdLine, i
q = Chr(34)
Set WshShell = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")

appDir = fso.GetParentFolderName(WScript.ScriptFullName)
If Not fso.FileExists(appDir & "\app.py") Then
candidates = Array( _
"C:\Users\chkam\OneDrive\Desktop\BrandFinder\VideoStudio", _
"C:\Users\chkam\OneDrive\Desktop\VideoStudio", _
"C:\Users\chkam\Desktop\BrandFinder\VideoStudio" _
)
For Each cand In candidates
If fso.FileExists(cand & "\app.py") Then
appDir = cand
Exit For
End If
Next
MsgBox "VideoStudio app.py was not found next to this launcher:" & vbCrLf & appDir, _
vbExclamation, "VideoStudio Pro"
WScript.Quit 1
End If

logPath = appDir & "\outputs\logs\launch.log"
Expand All @@ -41,7 +33,7 @@ If ServerUp() Then
WScript.Quit
End If

py = "C:\Users\chkam\AppData\Local\Programs\Python\Python314\python.exe"
py = appDir & "\.venv\Scripts\python.exe"
If Not fso.FileExists(py) Then py = "python.exe"

If Not fso.FolderExists(appDir & "\outputs") Then fso.CreateFolder(appDir & "\outputs")
Expand Down
88 changes: 52 additions & 36 deletions test_gemini_features.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,57 @@
"""Opt-in live smoke check for the configured Gemini and Imagen APIs.

Run explicitly with ``python test_gemini_features.py``; this is intentionally
not executed during test discovery because it consumes network/API quota.
"""

import os
import sys
from pathlib import Path

from dotenv import load_dotenv

# Load environment variables safely
load_dotenv(Path(__file__).parent / ".env")
load_dotenv()

key = os.environ.get("GEMINI_API_KEY")
print(f"GEMINI_API_KEY configured: {bool(key)}")

from google import genai

client = genai.Client(api_key=key)

try:
resp = client.models.generate_content(
model='gemini-2.5-flash',
contents='You are a Veo 3 cinematic prompt director. Expand: "Golden eagle soaring in mountains" into 1 cinematic prompt sentence.'
)
print("Gemini 2.5 Flash Response:")
print(resp.text)
except Exception as e:
print("Gemini text failed:", e)

# Test Image generation if supported
try:
img_resp = client.models.generate_images(
model='imagen-3.0-generate-002',
prompt='A majestic golden eagle soaring over snow covered mountain peaks at sunrise, 8k cinematic photography',
config=dict(
number_of_images=1,
aspect_ratio='16:9',

def main() -> int:
load_dotenv(Path(__file__).parent / ".env")
load_dotenv()
key = os.environ.get("GEMINI_API_KEY")
if not key:
print("GEMINI_API_KEY is not configured; skipping live API checks.")
return 0

from google import genai

client = genai.Client(api_key=key)
try:
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents=(
'You are a Veo 3 cinematic prompt director. Expand: '
'"Golden eagle soaring in mountains" into 1 cinematic prompt sentence.'
),
)
print("Gemini 2.5 Flash Response:")
print(resp.text)
except Exception as exc:
print("Gemini text check failed:", exc)
return 1

try:
img_resp = client.models.generate_images(
model="imagen-3.0-generate-002",
prompt=(
"A majestic golden eagle soaring over snow covered mountain peaks "
"at sunrise, 8k cinematic photography"
),
config={"number_of_images": 1, "aspect_ratio": "16:9"},
)
)
print("Imagen 3 image generation succeeded!")
if img_resp.generated_images:
print("Generated image bytes length:", len(img_resp.generated_images[0].image.image_bytes))
except Exception as e:
print("Imagen 3 test note:", e)
print("Imagen image generation succeeded.")
if img_resp.generated_images:
print("Generated image bytes length:", len(img_resp.generated_images[0].image.image_bytes))
except Exception as exc:
print("Imagen check failed:", exc)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading