Compare commits
4 Commits
claude/ima
...
claude/ado
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05ee739f57 | ||
|
|
a18aa9f316 | ||
|
|
d00e746727 | ||
|
|
a4f3b1832e |
17
.github/workflows/cto-review.yml
vendored
17
.github/workflows/cto-review.yml
vendored
@@ -123,17 +123,18 @@ jobs:
|
||||
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=50
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||
DIFF=$(git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000)
|
||||
# Write the diff to a file, not a step output / env var. Passing a
|
||||
# ~400KB diff through the process env (execve argv+envp) exceeds the
|
||||
# runner ARG_MAX and fails the next step with "Argument list too long"
|
||||
# (issue #110). File transport also sidesteps the ~1MB step-output cap.
|
||||
git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000 > /tmp/cto-review-diff.txt
|
||||
FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
||||
SIZE=$(echo -n "$DIFF" | wc -c)
|
||||
SIZE=$(wc -c < /tmp/cto-review-diff.txt)
|
||||
{
|
||||
echo "size=$SIZE"
|
||||
echo "files<<EOF_FILES"
|
||||
echo "$FILES"
|
||||
echo "EOF_FILES"
|
||||
echo "diff<<EOF_DIFF"
|
||||
echo "$DIFF"
|
||||
echo "EOF_DIFF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Load repo conventions
|
||||
@@ -161,7 +162,7 @@ jobs:
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
DIFF: ${{ steps.diff.outputs.diff }}
|
||||
DIFF_PATH: /tmp/cto-review-diff.txt
|
||||
DIFF_SIZE: ${{ steps.diff.outputs.size }}
|
||||
CHANGED_FILES: ${{ steps.diff.outputs.files }}
|
||||
CONVENTIONS: ${{ steps.conventions.outputs.content }}
|
||||
@@ -229,7 +230,9 @@ jobs:
|
||||
user_blocks.append(f"**Changed files:**\n```\n{os.environ['CHANGED_FILES']}\n```\n")
|
||||
if os.environ.get("CONVENTIONS", "").strip():
|
||||
user_blocks.append(f"# Repo CONVENTIONS.md\n{os.environ['CONVENTIONS']}\n")
|
||||
user_blocks.append(f"# Diff\n```diff\n{os.environ['DIFF']}\n```\n")
|
||||
with open(os.environ["DIFF_PATH"], encoding="utf-8", errors="replace") as _df:
|
||||
_diff_text = _df.read()
|
||||
user_blocks.append(f"# Diff\n```diff\n{_diff_text}\n```\n")
|
||||
user_message = "\n\n".join(user_blocks)
|
||||
|
||||
tools = [{
|
||||
|
||||
94
.github/workflows/deploy.yml
vendored
94
.github/workflows/deploy.yml
vendored
@@ -28,92 +28,10 @@ jobs:
|
||||
-not -path './.github/*' \
|
||||
-print0 | xargs -0 -n1 -P4 php -l > /dev/null
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
deploy:
|
||||
name: Deploy (shared pipeline)
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||
- name: Deploy via rsync → staging
|
||||
run: |
|
||||
rsync -avz --delete \
|
||||
--exclude '.git' --exclude '.github' \
|
||||
--exclude 'tests' --exclude 'phpunit.xml.dist' \
|
||||
--exclude 'composer.json' --exclude 'composer.lock' \
|
||||
--exclude 'vendor' --exclude '.distignore' \
|
||||
--exclude 'ARCHITECTURE.md' --exclude 'CONVENTIONS.md' \
|
||||
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }}" \
|
||||
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:~/domains/peptiderepo.com/public_html/staging/wp-content/plugins/prautoblogger/
|
||||
- name: Staging health check
|
||||
run: |
|
||||
sleep 5
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.peptiderepo.com/ || echo "000")
|
||||
echo "Staging responded: HTTP $STATUS"
|
||||
[ "$STATUS" = "000" ] && echo "Warning: staging DNS may still be propagating" || true
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
|
||||
smoke:
|
||||
name: Smoke tests (staging)
|
||||
needs: deploy-staging
|
||||
continue-on-error: true
|
||||
uses: peptiderepo/peptide-e2e/.github/workflows/smoke.yml@main
|
||||
secrets:
|
||||
STAGING_WP_ADMIN_USER: ${{ secrets.STAGING_WP_ADMIN_USER }}
|
||||
STAGING_WP_ADMIN_PASSWORD: ${{ secrets.STAGING_WP_ADMIN_PASSWORD }}
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
needs: [validate, deploy-staging]
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||
- name: Deploy via rsync → production
|
||||
run: |
|
||||
rsync -avz --delete \
|
||||
--exclude '.git' --exclude '.github' \
|
||||
--exclude 'tests' --exclude 'phpunit.xml.dist' \
|
||||
--exclude 'composer.json' --exclude 'composer.lock' \
|
||||
--exclude 'vendor' --exclude '.distignore' \
|
||||
--exclude 'ARCHITECTURE.md' --exclude 'CONVENTIONS.md' \
|
||||
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }}" \
|
||||
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:~/domains/peptiderepo.com/public_html/wp-content/plugins/prautoblogger/
|
||||
- name: Purge LiteSpeed Cache
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} \
|
||||
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} \
|
||||
"cd ~/domains/peptiderepo.com/public_html && \
|
||||
wp litespeed-purge all 2>/dev/null || \
|
||||
(find wp-content/litespeed/ -type f -delete 2>/dev/null; touch .lscache_purge 2>/dev/null) || true"
|
||||
- name: Production health check
|
||||
run: |
|
||||
sleep 5
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com)
|
||||
if [ "$STATUS" != "200" ] && [ "$STATUS" != "403" ]; then
|
||||
echo "ERROR: Site returned unexpected HTTP $STATUS after deploy"
|
||||
exit 1
|
||||
fi
|
||||
echo "Production healthy (HTTP $STATUS)"
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
- name: Deploy summary
|
||||
run: |
|
||||
echo "## Deployment Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Deployed to:** peptiderepo.com" >> $GITHUB_STEP_SUMMARY
|
||||
uses: peptiderepo/peptide-e2e/.github/workflows/deploy-app.yml@main
|
||||
with:
|
||||
target_path: wp-content/plugins/prautoblogger
|
||||
secrets: inherit
|
||||
|
||||
22
.github/workflows/main-push-audit.yml
vendored
22
.github/workflows/main-push-audit.yml
vendored
@@ -29,12 +29,22 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const sha = context.sha;
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: sha,
|
||||
});
|
||||
const merged = prs.find(p => p.merged_at);
|
||||
// Squash-merge -> PR association is eventually-consistent; poll briefly
|
||||
// before concluding this was a direct push (avoids false positives on merges).
|
||||
async function findMergedPR() {
|
||||
for (let attempt = 1; attempt <= 5; attempt++) {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: sha,
|
||||
});
|
||||
const m = prs.find(p => p.merged_at);
|
||||
if (m) return m;
|
||||
if (attempt < 5) await new Promise(r => setTimeout(r, 10000));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const merged = await findMergedPR();
|
||||
if (merged) {
|
||||
core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`);
|
||||
return;
|
||||
|
||||
@@ -147,6 +147,7 @@ prautoblogger/
|
||||
│ │ ├── class-post-assembler.php # Post-creation helpers: taxonomy, log linking, images, sanitization
|
||||
│ │ ├── class-image-pipeline.php # Orchestrates A/B image generation (parallel via batch)
|
||||
│ │ ├── class-image-prompt-builder.php # Generates visual prompts from article/source data
|
||||
│ │ ├── class-image-template-filler.php # Fills the editorial style template's {{ topic_summary }} slot (v0.16.0)
|
||||
│ │ ├── class-image-media-sideloader.php # Imports images into WordPress media library
|
||||
│ │ ├── class-cost-tracker.php # Logs all API costs, enforces budget limits
|
||||
│ │ ├── class-logger.php # Structured logging singleton (error/warning/info/debug)
|
||||
@@ -437,13 +438,14 @@ All prefixed with `prautoblogger_`:
|
||||
| `prautoblogger_schedule_time` | Daily generation time (HH:MM, default: '03:00') |
|
||||
| `prautoblogger_image_model` | Image model slug from `Image_Model_Registry::get_models()`; provider is derived from this on save |
|
||||
| `prautoblogger_image_provider` | Derived from the chosen model on save (v0.8.0+); not editable in admin UI |
|
||||
| `prautoblogger_image_prompt_instructions` | System prompt given to the image rewriter LLM (v0.8.0+); falls back to `Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT` when empty |
|
||||
| `prautoblogger_image_prompt_instructions` | System prompt given to the image rewriter LLM (v0.8.0+); falls back to `Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT` when empty. v0.16.0: the default now instructs the LLM to emit a 1-2 sentence editorial topic/mechanism summary as the SCENE (was a comic gag) |
|
||||
| `prautoblogger_image_nsfw_retry` | Toggle: retry NSFW-blocked image slots with a rule-based fallback prompt (default: '1') |
|
||||
| `prautoblogger_migrated_image_provider_v080` | One-shot migration flag — auto-heals mismatched provider/model pairs on first admin_init after v0.8.0 upgrade |
|
||||
| `prautoblogger_article_font_family` | Font family key: 'default', 'inter', 'georgia', 'merriweather', 'lora', 'open_sans', 'roboto', 'system' |
|
||||
| `prautoblogger_article_font_size` | Body font size in px (0 = theme default). Recommended: 16–18. |
|
||||
| `prautoblogger_table_borders` | Toggle: add borders/padding/striping to tables (default: '1') |
|
||||
| `prautoblogger_image_style_suffix` | Text appended to every image prompt (default: CEO-locked 90s infomercial prompt) |
|
||||
| `prautoblogger_image_style_template` | Full image prompt template (v0.16.0+); contains exactly one `{{ topic_summary }}` token filled per-article with the rewriter scene. Default = `PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE` (editorial scientific illustration). Replaces the Style Suffix. Validated on save (single-token, brief A5) |
|
||||
| `prautoblogger_image_style_suffix` | DEPRECATED (v0.16.0). Former comic style suffix appended to every image prompt; no longer read by the builder. Mirrored to `prautoblogger_image_style_suffix_deprecated` for one cycle and not deleted yet |
|
||||
| `prautoblogger_openrouter_model_registry` | Normalized OpenRouter model list (JSON array, daily refresh, serves as durable cache) |
|
||||
| `prautoblogger_openrouter_model_registry_fetched_at` | Unix timestamp of last successful model registry refresh |
|
||||
|
||||
@@ -575,6 +577,16 @@ Reddit rejected our OAuth API application (April 2026). We initially switched to
|
||||
|
||||
The admin model picker (v1) needs to list OpenRouter models with pricing and capabilities. We fetch `https://openrouter.ai/api/v1/models` (free, unauthenticated) once daily and store the normalized payload in a WP option fronted by a 24h transient. On stale-and-fetch-fails, we serve last-good + surface a warning. The registry class (`class-open-router-model-registry.php`) takes all config via constructor (option name, transient name, endpoint URL) — no PRAUTOBLOGGER_* constants inside the class body. Phase 2 lifts it into a shared Composer package with zero internal edits. Phase 3 adds a parallel `Cloudflare_WorkersAI_Model_Registry` behind the same interface. Capability vocabulary: `text→text`, `text+image→text`, `text+audio→text`, `text→image`, `text→audio`, `text→video`, `text→embedding`. Cost: $0/month.
|
||||
|
||||
### #20: Editorial illustration prompt + style-template setting (v0.16.0, May 2026)
|
||||
|
||||
Commit 1 of the in-plugin editorial image pipeline (brief `docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md`). The image style pivots from a single-panel newspaper comic to a **text-free editorial scientific illustration**. Two changes ship here; the deterministic PHP composer is a later commit.
|
||||
|
||||
1. **Prompt structure.** `Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT` now asks the rewriter LLM for a concise 1-2 sentence topic/mechanism summary (one concrete centered focal subject, no text/people-as-gag/logos) as the SCENE, keeping the short CAPTION line for the unchanged HTML-caption-below-image path. The scene/caption parsing contract (`Image_Scene_Parser`) is unchanged.
|
||||
2. **Template substitution.** The old `trim($scene . ' ' . $style_suffix)` concat is replaced by `PRAutoBlogger_Image_Template_Filler::fill($scene)` in all three entry points (`build_article_prompt`, `build_source_prompt`, `build_fallback_prompt`). The filler resolves the active template (admin override → default), strips control chars and clamps the summary length (brief A5), then substitutes the single `{{ topic_summary }}` token. Degradation (brief A5/A6): if the template lacks exactly one token it appends the summary and logs a warning; if the summary is empty it emits the style-only prompt — a blank or token-only prompt is never sent to the provider.
|
||||
3. **Setting.** New textarea `prautoblogger_image_style_template` (default `PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE`) replaces the Style Suffix field. On save it is sanitised with `sanitize_textarea_field` and validated to contain exactly one token (`Image_Template_Filler::sanitize_for_save`). The old `prautoblogger_image_style_suffix` is mirrored to `..._deprecated` for one cycle and not deleted (one-time migration `prautoblogger_migrated_style_template_v0160`).
|
||||
|
||||
Provider/model are unchanged — only the prompt text changes, so production keeps emitting via Runware FLUX.1 schnell but now produces text-free editorial base images.
|
||||
|
||||
### #17: Runware as default image backend (v0.9.0, Apr 2026)
|
||||
|
||||
After a comic-style A/B round in Apr 2026, FLUX.1 schnell via Runware was chosen as the default image backend. Cost is ~$0.0006/image vs ~$0.039/image for Gemini 2.5 Flash Image via OpenRouter (≈65× cheaper), and the looser schnell fidelity reads as editorial-cartoon style — a feature, not a bug, for our single-panel comic aesthetic. FLUX.1 dev (~$0.02/image, 28 steps) remains available as an opt-in for higher-fidelity runs. The Runware layer mirrors the OpenRouter split (interface + provider + support + pricing + batch), all under the 300-line cap. The batch class dispatches `imageInference` POSTs via `curl_multi_init/exec/select` — wall-clock time for the A/B pair drops to ≈ the slowest single image. A v0.9.0 one-time migration (`prautoblogger_migrated_default_image_v090`) flips sites still on the legacy default; explicit user selections are preserved. Cloudflare Workers AI was removed as an image provider in v0.10.0 (see ADR #16 note below).
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -5,6 +5,43 @@ All notable changes to PRAutoBlogger will be documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.16.0] - 2026-05-29
|
||||
|
||||
### Changed
|
||||
- **Editorial image prompt (Commit 1 of the in-plugin editorial image pipeline).**
|
||||
The image style pivots from a single-panel newspaper comic to a text-free
|
||||
editorial scientific illustration. The rewriter LLM now emits a concise
|
||||
1-2 sentence topic/mechanism summary as the SCENE (one centered focal
|
||||
subject, no text/people-as-gag/logos), keeping the short CAPTION line for the
|
||||
unchanged HTML-caption-below-image behavior. The scene/caption parsing
|
||||
contract is unchanged. Provider/model are unchanged — production keeps
|
||||
generating via Runware FLUX.1 schnell but now produces text-free editorial
|
||||
base images. See `docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md`.
|
||||
- The prompt builder now substitutes the rewriter scene into an editable style
|
||||
template's `{{ topic_summary }}` slot via the new
|
||||
`PRAutoBlogger_Image_Template_Filler`, replacing the old
|
||||
`scene + style suffix` concatenation, across all three entry points
|
||||
(`build_article_prompt`, `build_source_prompt`, `build_fallback_prompt`).
|
||||
Before substitution the scene is stripped of control characters and
|
||||
length-clamped (brief A5). If the template lacks exactly one token the
|
||||
summary is appended and a warning logged; if the summary is empty the
|
||||
style-only prompt is emitted — a blank or token-only prompt is never sent to
|
||||
the provider (brief A5/A6).
|
||||
|
||||
### Added
|
||||
- New setting **Style Template** (`prautoblogger_image_style_template`,
|
||||
textarea) replacing the Style Suffix field. Default =
|
||||
`PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE` (the editorial template). On save
|
||||
it is validated to contain exactly one `{{ topic_summary }}` token; an invalid
|
||||
template keeps the previous value and surfaces a settings error.
|
||||
|
||||
### Deprecated
|
||||
- `prautoblogger_image_style_suffix` is deprecated and no longer read by the
|
||||
prompt builder. A one-time migration (`prautoblogger_migrated_style_template_v0160`)
|
||||
mirrors its value to `prautoblogger_image_style_suffix_deprecated` for one
|
||||
version cycle and seeds the new template default. The old option is NOT
|
||||
deleted this cycle.
|
||||
|
||||
## [0.15.1] - 2026-05-14
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -330,6 +330,11 @@ class PRAutoBlogger_Admin_Page {
|
||||
if ( 'prautoblogger_image_model' === $option_name ) {
|
||||
return $this->sanitize_image_model( (string) $value );
|
||||
}
|
||||
if ( 'prautoblogger_image_style_template' === $option_name ) {
|
||||
// Multi-line template: validation + textarea-safe sanitisation
|
||||
// (incl. the single-token check, brief A5) lives in the filler.
|
||||
return PRAutoBlogger_Image_Template_Filler::sanitize_for_save( (string) $value );
|
||||
}
|
||||
return sanitize_text_field( (string) $value );
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ declare(strict_types=1);
|
||||
*
|
||||
* What: Declarative field definitions for operational settings sections.
|
||||
* Who calls it: PRAutoBlogger_Settings_Fields::get_fields() merges these in.
|
||||
* Dependencies: PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX constants,
|
||||
* Dependencies: PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE constants,
|
||||
* PRAutoBlogger_Article_Typography (font choices), PRAutoBlogger_Image_Model_Registry.
|
||||
*
|
||||
* @see admin/class-settings-fields.php — Core fields and sections; calls get_extended_fields().
|
||||
@@ -235,12 +235,12 @@ class PRAutoBlogger_Settings_Fields_Extended {
|
||||
'icon' => '🔑',
|
||||
),
|
||||
array(
|
||||
'id' => 'prautoblogger_image_style_suffix',
|
||||
'label' => __( 'Style Suffix', 'prautoblogger' ),
|
||||
'id' => 'prautoblogger_image_style_template',
|
||||
'label' => __( 'Style Template', 'prautoblogger' ),
|
||||
'type' => 'textarea',
|
||||
'section' => 'prautoblogger_images',
|
||||
'default' => PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX,
|
||||
'description' => __( 'Appended to every image prompt. Controls visual look. Changing mid-run causes visible style drift.', 'prautoblogger' ),
|
||||
'default' => PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE,
|
||||
'description' => __( 'The full image prompt template. Must contain exactly one {{ topic_summary }} token, which is filled with a 1-2 sentence topic/mechanism summary per article. The rest is brand-locked editorial style. Replaces the old Style Suffix (now deprecated). Changing mid-run causes visible style drift.', 'prautoblogger' ),
|
||||
),
|
||||
array(
|
||||
'id' => 'prautoblogger_image_prompt_instructions',
|
||||
@@ -248,7 +248,7 @@ class PRAutoBlogger_Settings_Fields_Extended {
|
||||
'type' => 'textarea',
|
||||
'section' => 'prautoblogger_images',
|
||||
'default' => PRAutoBlogger_Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT,
|
||||
'description' => __( 'System prompt given to the rewriter LLM that turns each article into a SCENE + CAPTION for the image generator. Changing this reshapes the look of all future images. Leave blank to use the default.', 'prautoblogger' ),
|
||||
'description' => __( 'System prompt given to the rewriter LLM that turns each article into a SCENE (a 1-2 sentence editorial topic/mechanism summary, substituted into the Style Template) plus a CAPTION (HTML text shown below the image). Changing this reshapes the look of all future images. Leave blank to use the default.', 'prautoblogger' ),
|
||||
),
|
||||
array(
|
||||
'id' => 'prautoblogger_image_nsfw_retry',
|
||||
|
||||
@@ -267,6 +267,24 @@ class PRAutoBlogger {
|
||||
update_option( 'prautoblogger_migrated_style_suffix_v4', '1' );
|
||||
}
|
||||
|
||||
// One-time migration (v0.16.0): editorial pivot. The comic Style Suffix
|
||||
// is replaced by the editorial Style Template. Mirror the old suffix
|
||||
// value into a deprecated-keyed option for one version cycle (so a
|
||||
// customised value is recoverable / auditable) and seed the new
|
||||
// template with its editorial default. The old option is intentionally
|
||||
// NOT deleted this cycle. See docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md §8 Commit 1.
|
||||
if ( ! get_option( 'prautoblogger_migrated_style_template_v0160' ) ) {
|
||||
$old_suffix = get_option( 'prautoblogger_image_style_suffix', '' );
|
||||
if ( '' !== $old_suffix && false === get_option( 'prautoblogger_image_style_suffix_deprecated', false ) ) {
|
||||
update_option( 'prautoblogger_image_style_suffix_deprecated', $old_suffix );
|
||||
}
|
||||
// Only seed the new template if the admin has not already set one.
|
||||
if ( '' === (string) get_option( 'prautoblogger_image_style_template', '' ) ) {
|
||||
update_option( 'prautoblogger_image_style_template', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE );
|
||||
}
|
||||
update_option( 'prautoblogger_migrated_style_template_v0160', '1' );
|
||||
}
|
||||
|
||||
// v0.9.0 — Runware as default image model. v0.10.0 — remove CF Workers AI.
|
||||
PRAutoBlogger_Activator::migrate_default_image_v090();
|
||||
PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
@@ -6,13 +6,16 @@ declare(strict_types=1);
|
||||
*
|
||||
* Builds image prompts from article content or source data.
|
||||
*
|
||||
* Uses a cheap LLM (via OpenRouter) to distill article content into concise
|
||||
* visual scene descriptions optimised for image generation. Falls back to rule-based
|
||||
* Uses a cheap LLM (via OpenRouter) to distill article content into a concise
|
||||
* 1-2 sentence editorial topic/mechanism summary, then substitutes that summary
|
||||
* into the admin-editable editorial style template (the {{ topic_summary }}
|
||||
* slot) to produce a text-free image-gen prompt. Falls back to rule-based
|
||||
* synthesis if the LLM call fails, so image generation never blocks on a
|
||||
* prompt-rewriting outage.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger_Image_Pipeline during content generation run.
|
||||
* Dependencies: PRAutoBlogger_OpenRouter_Provider (LLM call),
|
||||
* PRAutoBlogger_Image_Template_Filler (template substitution),
|
||||
* PRAutoBlogger_Cost_Tracker (logs prompt-rewrite cost),
|
||||
* PRAutoBlogger_Logger (diagnostics).
|
||||
*
|
||||
@@ -27,34 +30,39 @@ class PRAutoBlogger_Image_Prompt_Builder {
|
||||
* prompts. Exposed as a `public const` so the admin-settings layer can
|
||||
* use it as the default value for `prautoblogger_image_prompt_instructions`.
|
||||
* The option, when non-empty, wins at call time (see rewrite_via_llm).
|
||||
*
|
||||
* Editorial pivot (v0.16.0): the model emits a concise topic/mechanism
|
||||
* summary as the SCENE — concrete, single centered focal subject, no text,
|
||||
* no people-as-gag, no logos — which the prompt builder substitutes into
|
||||
* the editorial style template's {{ topic_summary }} slot. The CAPTION line
|
||||
* is preserved unchanged for the existing HTML-caption-below-image path.
|
||||
*/
|
||||
public const REWRITER_SYSTEM_PROMPT = <<<'PROMPT'
|
||||
You are a comedy writer and single-panel cartoon creator, like Gary Larson (The Far Side) meets science humor.
|
||||
You write subject descriptions for a text-free editorial science illustration about peptides, supplements, or biohacking.
|
||||
|
||||
Given an article title and summary about peptides, supplements, or biohacking, create a SINGLE-PANEL COMIC concept. Output TWO parts separated by a blank line:
|
||||
Given an article title and summary, output TWO parts separated by a blank line:
|
||||
|
||||
SCENE: One sentence describing the visual gag — a funny, absurd, or ironic situation related to the article's topic. Favor CONCRETE, HUMAN-SCALE scenes: people reacting to things, doctors and patients, scientists in a lab, gym bros with peptide vials, partners at a kitchen table, ordinary objects in ordinary rooms. Keep it simple — one clear visual joke, 1-3 characters max.
|
||||
SCENE: A concise 1-2 sentence description of the article's core topic or mechanism, written as the SUBJECT of a single editorial illustration. Name ONE concrete, centered focal subject and the few supporting visual elements around it (e.g. a labelled-free vial and dropper, a stylised receptor on a cell membrane, a molecular chain, a dosing syringe, an organ cross-section). Favor a clear central focal point with margin around it so the image crops well. This is image-gen direction, not prose.
|
||||
|
||||
CAPTION: A short, punchy caption or speech bubble line (under 15 words) that delivers the punchline. The humor should be dry, nerdy, and accessible — the reader should chuckle even if they only half-understand the science.
|
||||
CAPTION: A short, informative caption line (under 15 words) for display as HTML text below the image. Plain and editorial, not a joke.
|
||||
|
||||
Rules:
|
||||
- The joke MUST relate to the article's actual subject matter, not generic science humor
|
||||
- DO NOT personify molecules, peptides, hormones, proteins, or other chemical structures (no "anthropomorphic molecule", no "cartoon hormone"). Image models render anthropomorphized chemistry as incoherent blobs. If the article is about a substance, show a PERSON reacting to or using the substance; if the article is abstract (a mechanism, a process, a concept), fall back on a human observer reacting to its effect.
|
||||
- Characters can have simple cartoon faces (round heads, dot eyes, expressive eyebrows)
|
||||
- Keep the scene physically simple — few objects, clear staging, easy to read at thumbnail size
|
||||
- The caption should work as a standalone joke even without the image
|
||||
- No logos, watermarks, or branding
|
||||
- The SCENE must depict the article's actual subject/mechanism, not a generic science scene.
|
||||
- NO text, words, captions, speech bubbles, labels, or logos described in the SCENE — the illustration is text-free; text is rendered separately.
|
||||
- NO people used as a visual gag and NO cartoon/comic framing. A human anatomical element (a cell, an organ, a hand holding a vial) is fine if it is the legitimate subject.
|
||||
- Do NOT personify molecules, peptides, hormones, or proteins (no "smiling molecule"); depict them as clean stylised diagrams or structures.
|
||||
- Keep the staging simple with one clear focal subject, readable at small sizes.
|
||||
- Output ONLY the scene and caption. No preamble, no explanation.
|
||||
|
||||
Example output format:
|
||||
A muscular gym bro stares, deflated, at a tiny glass vial in his hand while his friend squats a barbell effortlessly in the background.
|
||||
A single glass peptide vial on a clean surface with a fine dropper above it, a faint stylised molecular chain arcing behind as a backdrop, centered with generous negative space.
|
||||
|
||||
"They told me the peptide did the work, not the squats."
|
||||
A concentrated look at how the compound is prepared before use.
|
||||
PROMPT;
|
||||
|
||||
/**
|
||||
* Max tokens for the rewriter response. Comic concepts need more room
|
||||
* for the scene description plus the caption punchline.
|
||||
* Max tokens for the rewriter response. Enough room for the 1-2 sentence
|
||||
* topic/mechanism summary plus the short editorial caption line.
|
||||
*/
|
||||
private const REWRITER_MAX_TOKENS = 180;
|
||||
|
||||
@@ -85,8 +93,9 @@ PROMPT;
|
||||
* Build a visual prompt from finished article content.
|
||||
*
|
||||
* Tries LLM rewriting first; falls back to rule-based synthesis on
|
||||
* failure. Splits scene (for image gen) from caption (HTML below the
|
||||
* image) and appends the style suffix.
|
||||
* failure. Splits scene (the topic summary, for image gen) from caption
|
||||
* (HTML below the image) and substitutes the scene into the editorial
|
||||
* style template's {{ topic_summary }} slot.
|
||||
*
|
||||
* @param array{post_title?: string, post_content?: string, suggested_title?: string} $article_data
|
||||
* @return array{prompt: string, caption: string}
|
||||
@@ -98,10 +107,8 @@ PROMPT;
|
||||
|
||||
$parsed = $this->rewrite_via_llm( $title, $first_para );
|
||||
|
||||
$style_suffix = $this->get_style_suffix();
|
||||
|
||||
return array(
|
||||
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
|
||||
'prompt' => PRAutoBlogger_Image_Template_Filler::fill( $parsed['scene'] ),
|
||||
'caption' => $parsed['caption'],
|
||||
);
|
||||
}
|
||||
@@ -120,10 +127,8 @@ PROMPT;
|
||||
|
||||
$parsed = $this->rewrite_via_llm( $title, $context );
|
||||
|
||||
$style_suffix = $this->get_style_suffix();
|
||||
|
||||
return array(
|
||||
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
|
||||
'prompt' => PRAutoBlogger_Image_Template_Filler::fill( $parsed['scene'] ),
|
||||
'caption' => $parsed['caption'],
|
||||
);
|
||||
}
|
||||
@@ -260,10 +265,9 @@ PROMPT;
|
||||
* @return array{prompt: string, caption: string}
|
||||
*/
|
||||
public function build_fallback_prompt( string $title ): array {
|
||||
$parsed = PRAutoBlogger_Image_Scene_Parser::synthesize_visual_concepts_fallback( $title, '' );
|
||||
$style_suffix = $this->get_style_suffix();
|
||||
$parsed = PRAutoBlogger_Image_Scene_Parser::synthesize_visual_concepts_fallback( $title, '' );
|
||||
return array(
|
||||
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
|
||||
'prompt' => PRAutoBlogger_Image_Template_Filler::fill( $parsed['scene'] ),
|
||||
'caption' => $parsed['caption'],
|
||||
);
|
||||
}
|
||||
@@ -281,20 +285,4 @@ PROMPT;
|
||||
$override = (string) get_option( 'prautoblogger_image_prompt_instructions', '' );
|
||||
return '' !== trim( $override ) ? $override : self::REWRITER_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image style suffix with empty-string safeguard.
|
||||
*
|
||||
* Treats both absent and empty string as "use default" to prevent
|
||||
* silent style loss if the admin settings field is saved empty.
|
||||
*
|
||||
* @return string The style suffix with safe fallback.
|
||||
*/
|
||||
private function get_style_suffix(): string {
|
||||
$style_suffix = (string) get_option( 'prautoblogger_image_style_suffix', '' );
|
||||
if ( '' === trim( $style_suffix ) ) {
|
||||
$style_suffix = PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX;
|
||||
}
|
||||
return $style_suffix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,22 +70,25 @@ class PRAutoBlogger_Image_Scene_Parser {
|
||||
/**
|
||||
* Rule-based fallback when LLM rewriting is unavailable.
|
||||
*
|
||||
* Kept deliberately simple — this only runs if OpenRouter is down.
|
||||
* Kept deliberately simple — this only runs if OpenRouter is down. Produces
|
||||
* a text-free editorial-illustration subject (not a comic) so the prompt
|
||||
* builder always has a non-empty topic summary to substitute into the
|
||||
* style template, never shipping a blank prompt to the provider (brief A6).
|
||||
*
|
||||
* @param string $title Main heading / topic.
|
||||
* @param string $supporting_text Additional context.
|
||||
* @return array{scene: string, caption: string} Scene for image gen, caption for HTML.
|
||||
*/
|
||||
public static function synthesize_visual_concepts_fallback( string $title, string $supporting_text ): array {
|
||||
// Fallback produces a simple comic concept when the LLM is unavailable.
|
||||
// Fallback produces a simple editorial-illustration subject when the LLM is unavailable.
|
||||
$scene = sprintf(
|
||||
'A cartoon scientist in a lab coat looking bewildered while examining something related to: %s.',
|
||||
'A clean, centered editorial science illustration representing the topic of %s, with a single clear focal subject and generous negative space.',
|
||||
$title
|
||||
);
|
||||
|
||||
return array(
|
||||
'scene' => trim( $scene ),
|
||||
'caption' => 'Science is full of surprises.',
|
||||
'caption' => '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
185
includes/core/class-image-template-filler.php
Normal file
185
includes/core/class-image-template-filler.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Fills the admin-editable editorial image-prompt template with the
|
||||
* rewriter-produced topic summary, replacing the old "scene + style suffix"
|
||||
* concatenation.
|
||||
*
|
||||
* The template (option `prautoblogger_image_style_template`, default
|
||||
* PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE) contains exactly one
|
||||
* `{{ topic_summary }}` token. This class:
|
||||
* - resolves the active template (admin override wins; blank → default),
|
||||
* - sanitises and length-clamps the topic summary (brief A5),
|
||||
* - substitutes the token, and
|
||||
* - degrades safely (brief A5/A6) so a blank or token-only prompt is never
|
||||
* emitted to the image provider.
|
||||
*
|
||||
* Who calls it: PRAutoBlogger_Image_Prompt_Builder (static call from each of
|
||||
* build_article_prompt / build_source_prompt / build_fallback_prompt).
|
||||
* Dependencies: PRAutoBlogger_Logger (diagnostics only).
|
||||
*
|
||||
* @see core/class-image-prompt-builder.php — Sole caller.
|
||||
* @see ARCHITECTURE.md — Image generation data flow.
|
||||
*/
|
||||
class PRAutoBlogger_Image_Template_Filler {
|
||||
|
||||
/** The single substitution token the template must contain. */
|
||||
public const TOKEN = '{{ topic_summary }}';
|
||||
|
||||
/**
|
||||
* Max characters retained from the rewriter topic summary before it is
|
||||
* substituted into the template. Keeps the final provider prompt bounded
|
||||
* even if the rewriter ignores its length guidance.
|
||||
*/
|
||||
public const MAX_SUMMARY_CHARS = 320;
|
||||
|
||||
/**
|
||||
* Build the final image-generation prompt from a rewriter scene.
|
||||
*
|
||||
* @param string $scene Raw topic/mechanism summary from the rewriter (the
|
||||
* "scene" half of the scene/caption contract).
|
||||
* @return string Provider-ready prompt; never empty.
|
||||
*/
|
||||
public static function fill( string $scene ): string {
|
||||
$template = self::resolve_template();
|
||||
$summary = self::sanitize_summary( $scene );
|
||||
|
||||
// Brief A6: never ship a blank or token-only prompt. If the rewriter
|
||||
// produced nothing usable, return the template with the token removed
|
||||
// only as a last resort — but the caller guards this by passing a
|
||||
// rule-based fallback scene, so an empty summary here is unexpected.
|
||||
if ( '' === $summary ) {
|
||||
PRAutoBlogger_Logger::instance()->warning(
|
||||
'Editorial prompt fill received an empty topic summary; emitting style-only prompt.',
|
||||
'image_template_filler'
|
||||
);
|
||||
return self::strip_token( $template );
|
||||
}
|
||||
|
||||
$token_count = substr_count( $template, self::TOKEN );
|
||||
|
||||
if ( 1 === $token_count ) {
|
||||
return trim( str_replace( self::TOKEN, $summary, $template ) );
|
||||
}
|
||||
|
||||
// Brief A5: admin removed or duplicated the token. Fall back to
|
||||
// appending the summary to the style text and log a warning rather
|
||||
// than shipping a malformed prompt.
|
||||
PRAutoBlogger_Logger::instance()->warning(
|
||||
sprintf(
|
||||
'Image style template has %d "%s" tokens (expected 1); appending summary instead of substituting.',
|
||||
$token_count,
|
||||
self::TOKEN
|
||||
),
|
||||
'image_template_filler'
|
||||
);
|
||||
|
||||
$base = self::strip_token( $template );
|
||||
return trim( $base . ' ' . $summary );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitise and validate the admin-submitted style template on save.
|
||||
*
|
||||
* Multi-line, so sanitised with sanitize_textarea_field() (not
|
||||
* sanitize_text_field(), which would strip newlines). Per brief A5 the
|
||||
* template must contain exactly one TOKEN; otherwise the previous value is
|
||||
* kept and a settings error is surfaced. A blank submission falls back to
|
||||
* the editorial default so image generation can never be bricked.
|
||||
*
|
||||
* @param string $value Submitted template.
|
||||
* @return string Sanitised, validated template.
|
||||
*/
|
||||
public static function sanitize_for_save( string $value ): string {
|
||||
$clean = sanitize_textarea_field( $value );
|
||||
|
||||
if ( '' === trim( $clean ) ) {
|
||||
return PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE;
|
||||
}
|
||||
|
||||
$token_count = substr_count( $clean, self::TOKEN );
|
||||
if ( 1 !== $token_count ) {
|
||||
add_settings_error(
|
||||
'prautoblogger_image_style_template',
|
||||
'prautoblogger_image_style_template_token',
|
||||
sprintf(
|
||||
/* translators: 1: required token, 2: number of tokens found. */
|
||||
esc_html__( 'The Style Template must contain exactly one %1$s token (found %2$d). Keeping the previous value.', 'prautoblogger' ),
|
||||
esc_html( self::TOKEN ),
|
||||
(int) $token_count
|
||||
)
|
||||
);
|
||||
return (string) get_option( 'prautoblogger_image_style_template', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE );
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active template: admin override wins; blank → default.
|
||||
*
|
||||
* @return string Non-empty template string.
|
||||
*/
|
||||
private static function resolve_template(): string {
|
||||
$override = (string) get_option( 'prautoblogger_image_style_template', '' );
|
||||
if ( '' !== trim( $override ) ) {
|
||||
return $override;
|
||||
}
|
||||
return PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip control characters and clamp length of the rewriter summary
|
||||
* (brief A5). Whitespace runs (including newlines) are collapsed to single
|
||||
* spaces so the summary sits cleanly inside the template.
|
||||
*
|
||||
* @param string $scene Raw rewriter scene.
|
||||
* @return string Sanitised, length-clamped summary (possibly empty).
|
||||
*/
|
||||
private static function sanitize_summary( string $scene ): string {
|
||||
// Remove control characters (C0/C1) except via whitespace collapse below.
|
||||
$clean = preg_replace( '/[\x00-\x1F\x7F]+/u', ' ', $scene );
|
||||
if ( null === $clean ) {
|
||||
// preg_replace can return null on bad UTF-8; degrade conservatively.
|
||||
$clean = preg_replace( '/[\x00-\x1F\x7F]+/', ' ', $scene );
|
||||
$clean = is_string( $clean ) ? $clean : '';
|
||||
}
|
||||
|
||||
// Collapse whitespace runs to single spaces.
|
||||
$collapsed = preg_replace( '/\s+/u', ' ', $clean );
|
||||
$clean = is_string( $collapsed ) ? $collapsed : $clean;
|
||||
$clean = trim( $clean );
|
||||
|
||||
if ( '' === $clean ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Clamp length (multibyte-aware where available).
|
||||
if ( function_exists( 'mb_strlen' ) ) {
|
||||
if ( mb_strlen( $clean ) > self::MAX_SUMMARY_CHARS ) {
|
||||
$clean = rtrim( mb_substr( $clean, 0, self::MAX_SUMMARY_CHARS ) ) . '...';
|
||||
}
|
||||
} elseif ( strlen( $clean ) > self::MAX_SUMMARY_CHARS ) {
|
||||
$clean = rtrim( substr( $clean, 0, self::MAX_SUMMARY_CHARS ) ) . '...';
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove every occurrence of the token from a template, tidying the
|
||||
* leftover whitespace so the residual style text is still well-formed.
|
||||
*
|
||||
* @param string $template Template possibly containing the token.
|
||||
* @return string Token-free, trimmed template.
|
||||
*/
|
||||
private static function strip_token( string $template ): string {
|
||||
$stripped = str_replace( self::TOKEN, '', $template );
|
||||
$stripped = preg_replace( '/[ \t]+/', ' ', $stripped );
|
||||
$stripped = is_string( $stripped ) ? $stripped : $template;
|
||||
return trim( $stripped );
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
* Plugin Name: PRAutoBlogger
|
||||
* Plugin URI: https://peptiderepo.com/prautoblogger
|
||||
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
|
||||
* Version: 0.15.1
|
||||
* Version: 0.16.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: PeptideRepo
|
||||
@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
| Defined here so every file in the plugin can reference paths, versions,
|
||||
| and limits without magic strings.
|
||||
*/
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.15.1' );
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.16.0' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
@@ -66,6 +66,22 @@ define(
|
||||
'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX',
|
||||
'Style: a single-panel newspaper comic in classic black ink on white paper, simple clean linework like The Far Side or B.C., slightly yellowed newsprint texture, hand-drawn crosshatching for shading, bold panel border, warm and humorous tone, visible in a newspaper comics section. IMPORTANT: Do not render any text, captions, speech bubbles, or words inside the image.'
|
||||
);
|
||||
// Default editorial-illustration prompt template for the image generator.
|
||||
// Updated 2026-05-29 (v0.16.0): pivot from single-panel comic to text-free
|
||||
// editorial scientific illustration. The {{ topic_summary }} token is filled
|
||||
// at build time with a 1-2 sentence topic/mechanism description from the
|
||||
// rewriter LLM; the rest is brand-locked. Replaces the old Style Suffix.
|
||||
// Users may override via Settings -> Images -> Style Template. Per brief A5
|
||||
// the template must contain exactly one {{ topic_summary }} token.
|
||||
define(
|
||||
'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE',
|
||||
"Editorial scientific illustration, flat vector aesthetic, limited palette of\n" .
|
||||
"teal (#1B8A92) and cream (#FAF6EE) with ink (#0D0D0D) line work and accents\n" .
|
||||
"of orange (#FF8A3D) and lime (#7FD600). Subject: {{ topic_summary }}.\n" .
|
||||
"Style reference: Quanta Magazine, The Pudding, scientific editorial illustration.\n" .
|
||||
"Composition: clear focal point, balanced negative space, subtle textural detail.\n" .
|
||||
'Mood: authoritative, clinical, calm. NO TEXT. NO PEOPLE. NO LOGOS. NO LABELS.'
|
||||
);
|
||||
// Opik observability (feature-flag gated).
|
||||
// API credentials must be defined in wp-config.php constants:
|
||||
// - PRAUTOBLOGGER_OPIK_API_KEY: API key for authentication
|
||||
|
||||
@@ -67,11 +67,20 @@ if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER' ) ) {
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER', 'runware' );
|
||||
}
|
||||
|
||||
// Default image style suffix used by ImagePromptBuilder and settings UI.
|
||||
// Default image style suffix used by legacy ImagePromptBuilder paths / settings UI.
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX' ) ) {
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX', 'Style: test infomercial style.' );
|
||||
}
|
||||
|
||||
// Editorial style template (v0.16.0): default for prautoblogger_image_style_template.
|
||||
// Must contain exactly one {{ topic_summary }} token (brief A5).
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE' ) ) {
|
||||
define(
|
||||
'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE',
|
||||
"Editorial scientific illustration. Subject: {{ topic_summary }}. NO TEXT. NO PEOPLE. NO LOGOS. NO LABELS."
|
||||
);
|
||||
}
|
||||
|
||||
// WordPress database constants used in $wpdb queries.
|
||||
if ( ! defined( 'ARRAY_A' ) ) {
|
||||
define( 'ARRAY_A', 'ARRAY_A' );
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Tests for PRAutoBlogger_Image_Prompt_Builder.
|
||||
*
|
||||
* Validates prompt generation from article content and source data,
|
||||
* including style suffix appending and HTML stripping.
|
||||
* including editorial-template substitution ({{ topic_summary }}) and HTML stripping.
|
||||
*
|
||||
* @package PRAutoBlogger\Tests\Core
|
||||
*/
|
||||
@@ -68,7 +68,9 @@ class ImagePromptBuilderTest extends BaseTestCase {
|
||||
[ 'prompt' => $scene, 'caption' => $caption ] = $builder->build_article_prompt( $article_data );
|
||||
|
||||
$this->assertStringContainsString( 'How to Train Your Dragon', $scene );
|
||||
// Style suffix should be appended.
|
||||
// Editorial template wraps the topic summary; token must be substituted.
|
||||
$this->assertStringNotContainsString( '{{ topic_summary }}', $scene );
|
||||
$this->assertStringContainsString( 'Editorial scientific illustration', $scene );
|
||||
$this->assertNotEmpty( $scene );
|
||||
$this->assertGreaterThan( 50, strlen( $scene ) );
|
||||
$this->assertIsString( $caption );
|
||||
|
||||
Reference in New Issue
Block a user