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
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,49 @@ AI-powered tool for generating YouTube Shorts / TikTok videos with script genera
- **Two pipelines**: normal (YouTube bg + optional image overlays) and images-only (AI/web images + overlay animation, no YouTube bg)
- **Typer CLI** — nested subcommands, auto-generated `--help`, shell completion
- **Batch Processing** — semicolon-separated subjects for multi-video runs
- **Hook Engine** — 7 viral hook styles (curiosity, counter-narrative, controversy, challenge, reveal, story) with pattern interrupt and curiosity gap mechanics

## Hook Styles

AutoShorts now supports multiple hook styles to optimize script engagement:

- `default` — Original curiosity-driven hook (backward compatible)
- `curiosity` — Creates information asymmetry (viewer knows something is important but not what)
- `counter` — Challenges common knowledge and assumptions
- `controversy` — Takes a strong stance that demands engagement
- `challenge` — Direct challenge to viewer's knowledge
- `reveal` — Promises a shocking fact upfront
- `story` — Opens a narrative arc with character and conflict

### Usage

```bash
autoshorts new explainer "Corinthians 2012 Libertadores" --hook-style controversy
autoshorts new explainer "Pelé 1000 goals" --hook-style curiosity
autoshorts new explainer "Neymar vs Ronaldo" --hook-style counter
```

### Examples

```bash
# Curiosity gap (works well for history/facts)
autoshorts new explainer "Pelé 1000 goals" --hook-style curiosity

# Counter-narrative (works well for debates)
autoshorts new explainer "Neymar vs Ronaldo" --hook-style counter

# Controversy (works well for opinions)
autoshorts new explainer "VAR no futebol brasileiro" --hook-style controversy

# Challenge (works well for trivia)
autoshorts new explainer "Primeiro estrangeiro no Brasil" --hook-style challenge

# Reveal (works well for secrets)
autoshorts new explainer "Flamengo 1981 bastidores" --hook-style reveal

# Story (works well for narratives)
autoshorts new explainer "Palmeiras 2006 quase rebaixado" --hook-style story
```

## Installation

Expand Down Expand Up @@ -111,9 +154,10 @@ AutoShorts/
│ │ └── explainer.py # ExplainerGenerator (both pipelines)
│ └── modules/ # Core modules
│ ├── config.py
│ ├── hook_engine.py # Hook Engine with 7 viral styles, pattern interrupt, curiosity gap
│ ├── image_searcher.py # Web/AI image search + NSFW filter
│ ├── logging_system.py
│ ├── script_generator.py # Script gen, fact verification, title validation
│ ├── script_generator.py # Script gen, fact verification, title validation, hook integration
│ ├── subtitle_system.py
│ ├── tts_system.py
│ ├── utils.py
Expand All @@ -125,6 +169,7 @@ AutoShorts/
│ ├── test_config.py
│ ├── test_edge_cases.py
│ ├── test_fluximages.py # Explainer generator tests
│ ├── test_hook_engine.py # Hook Engine (15+ tests)
│ ├── test_init.py
│ ├── test_integration.py
│ ├── test_script_generator.py
Expand Down
13 changes: 12 additions & 1 deletion src/autoshorts/cli/commands/explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from ...generators import VIDEO_TYPES
from ...modules import VideoMetadata, log, shutdown_computer
from ...modules.hook_engine import HookStyle
from ..new import new_app


Expand Down Expand Up @@ -42,6 +43,13 @@ def explainer_command(
"-c",
help="Custom instructions for the AI on how to craft the video alongside the theme",
),
hook_style: HookStyle = typer.Option(
HookStyle.DEFAULT,
"--hook-style",
"-hs",
case_sensitive=False,
help="Hook style for script generation (default, curiosity, counter, controversy, challenge, reveal, story)",
),
):
if no_images and images_only:
raise typer.BadParameter("--no-images and --images-only are mutually exclusive")
Expand All @@ -63,8 +71,9 @@ def explainer_command(
output_path = Path(output)
success_count = 0
total_count = len(subjects)

def _sanitize(s):
return re.sub(r'[\\/*?:"<>|]', "", s).replace(" ", "_").lower()[:20]
return re.sub(r'[\\/*?:\"<>|]', "", s).replace(" ", "_").lower()[:20]

for i, subj in enumerate(subjects, 1):
log(f"Processing {i}/{total_count}: {subj or 'youtube-url'}")
Expand All @@ -90,6 +99,7 @@ def _sanitize(s):
"no_web_search": no_web_search,
"batch": batch,
"custom_instructions": custom_instructions,
"hook_style": hook_style.value if isinstance(hook_style, HookStyle) else str(hook_style),
}, ensure_ascii=False),
)

Expand All @@ -103,6 +113,7 @@ def _sanitize(s):
image_source=images,
custom_instructions=custom_instructions,
metadata=metadata,
hook_style=hook_style,
)
success = asyncio.run(gen.generate())
if success:
Expand Down
21 changes: 12 additions & 9 deletions src/autoshorts/cli/commands/help_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ def help_command(

if args:
for name in args:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: has_commands = hasattr and isinstance(dict) or hasattr simplifies to hasattr(target, "commands"). First part is dead due to or hasattr. Suggest: if hasattr(target, "commands"): cmds = target.commands or {} for clarity. Also note this fix correctly handles TyperGroup not subclassing click.Group — confirmed it fixes 3 failing tests (29->32).

if isinstance(target, click.Group) and name in target.commands:
target = target.commands[name]
info_parts.append(name)
else:
typer.secho(
f"Error: No such command: autoshorts {' '.join(args)}",
fg="red",
)
raise typer.Exit(1)
if hasattr(target, "commands"):
cmds = getattr(target, "commands", {}) or {}
if name in cmds:
target = cmds[name]
info_parts.append(name)
continue

typer.secho(
f"Error: No such command: autoshorts {' '.join(args)}",
fg="red",
)
raise typer.Exit(1)

help_ctx = click.Context(target, info_name=" ".join(info_parts))
typer.echo(target.get_help(help_ctx))
Expand Down
4 changes: 4 additions & 0 deletions src/autoshorts/generators/explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
log,
setup_directories,
)
from ..modules.hook_engine import HookStyle


class ExplainerGenerator:
Expand All @@ -65,6 +66,7 @@ def __init__(
image_source: str = "web",
custom_instructions: str | None = None,
metadata: VideoMetadata | None = None,
hook_style: HookStyle = HookStyle.DEFAULT,
):
self.subject = subject
self.output = output
Expand All @@ -75,10 +77,12 @@ def __init__(
self.image_source = image_source
self.custom_instructions = custom_instructions
self.metadata = metadata or VideoMetadata()
self.hook_style = hook_style

self.script_generator = ScriptGenerator(
web_search=web_search,
custom_instructions=custom_instructions,
hook_style=hook_style,
)
self.tts_system = TTSSystem()
self.temp_dir = create_temp_dir()
Expand Down
3 changes: 3 additions & 0 deletions src/autoshorts/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
YOUTUBE_FORMAT,
YOUTUBE_MAX_HEIGHT,
)
from .hook_engine import HookEngine, HookStyle
from .image_searcher import ImageSearcher
from .logging_system import Colors, log
from .metadata import VideoMetadata
Expand Down Expand Up @@ -164,6 +165,8 @@
"log",
"Colors",
"ScriptGenerator",
"HookEngine",
"HookStyle",
"TTSSystem",
"VideoBackgroundManager",
"VideoCompositor",
Expand Down
Loading
Loading