Add GET /enhance/prompts (recent Enhance prompts) #199
No files matched your search
@@ -507,6 +507,10 @@ GET /enhance/templates
|
|||||||
POST /enhance {"path","template","prompt"?,"reference_path"?,"library"?} -> job
|
POST /enhance {"path","template","prompt"?,"reference_path"?,"library"?} -> job
|
||||||
GET /enhance/jobs/{id} -> job
|
GET /enhance/jobs/{id} -> job
|
||||||
GET /enhance/jobs?path=&library= -> {"jobs": [job, ...]} newest first, discarded hidden
|
GET /enhance/jobs?path=&library= -> {"jobs": [job, ...]} newest first, discarded hidden
|
||||||
|
GET /enhance/prompts?templates=a,b&limit=10 (templates optional = all; limit 1..50, default 10)
|
||||||
|
-> {"prompts": [{"prompt","template","last_used","uses"}]} distinct trimmed prompts, newest
|
||||||
|
use first; template/last_used from the newest job; discarded + failed jobs count;
|
||||||
|
DB only (no library scope, answers even when ComfyUI is down or Enhance is off)
|
||||||
GET /enhance/jobs/{id}/preview/{n} -> the staged PNG (relayed from ComfyUI /view)
|
GET /enhance/jobs/{id}/preview/{n} -> the staged PNG (relayed from ComfyUI /view)
|
||||||
POST /enhance/jobs/{id}/approve {"n"} -> a /image/versions `versions[]` entry
|
POST /enhance/jobs/{id}/approve {"n"} -> a /image/versions `versions[]` entry
|
||||||
POST /enhance/jobs/{id}/discard -> job (hidden from the list; ComfyUI untouched)
|
POST /enhance/jobs/{id}/discard -> job (hidden from the list; ComfyUI untouched)
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ const COLUMNS: &str = "id, original_hash, library_id, rel_path, template, prompt
|
|||||||
reference_path, comfy_prompt_id, status, error, queue_position, pending, saved, \
|
reference_path, comfy_prompt_id, status, error, queue_position, pending, saved, \
|
||||||
discarded, created_at, updated_at, run_ms";
|
discarded, created_at, updated_at, run_ms";
|
||||||
|
|
||||||
|
/// One distinct prompt used across jobs, newest use first.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, QueryableByName)]
|
||||||
|
pub struct RecentPrompt {
|
||||||
|
#[diesel(sql_type = Text)]
|
||||||
|
pub prompt: String,
|
||||||
|
/// Template of the newest job that used this prompt.
|
||||||
|
#[diesel(sql_type = Text)]
|
||||||
|
pub template: String,
|
||||||
|
/// `created_at` of the newest job that used it.
|
||||||
|
#[diesel(sql_type = BigInt)]
|
||||||
|
pub last_used: i64,
|
||||||
|
#[diesel(sql_type = BigInt)]
|
||||||
|
pub uses: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// What the poller / approve changes on an existing job.
|
/// What the poller / approve changes on an existing job.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct JobUpdate {
|
pub struct JobUpdate {
|
||||||
@@ -107,6 +122,14 @@ pub trait EnhanceJobDao: Sync + Send {
|
|||||||
/// Jobs the poller still has to advance (`queued` / `running`).
|
/// Jobs the poller still has to advance (`queued` / `running`).
|
||||||
fn list_active(&mut self) -> Result<Vec<EnhanceJobRow>, DbError>;
|
fn list_active(&mut self) -> Result<Vec<EnhanceJobRow>, DbError>;
|
||||||
fn update(&mut self, id: &str, update: &JobUpdate) -> Result<(), DbError>;
|
fn update(&mut self, id: &str, update: &JobUpdate) -> Result<(), DbError>;
|
||||||
|
/// Distinct non-empty prompts (compared trimmed), newest use first.
|
||||||
|
/// Counts every job, discarded and failed included. `templates` limits
|
||||||
|
/// to those template names; `None` means all.
|
||||||
|
fn recent_prompts(
|
||||||
|
&mut self,
|
||||||
|
templates: Option<&[String]>,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<RecentPrompt>, DbError>;
|
||||||
/// Hide a job from `list_for_hash`.
|
/// Hide a job from `list_for_hash`.
|
||||||
fn discard(&mut self, id: &str) -> Result<(), DbError>;
|
fn discard(&mut self, id: &str) -> Result<(), DbError>;
|
||||||
/// Delete jobs last touched before `cutoff` (unix seconds) that are not
|
/// Delete jobs last touched before `cutoff` (unix seconds) that are not
|
||||||
@@ -238,6 +261,36 @@ impl EnhanceJobDao for SqliteEnhanceJobDao {
|
|||||||
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
|
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recent_prompts(
|
||||||
|
&mut self,
|
||||||
|
templates: Option<&[String]>,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<RecentPrompt>, DbError> {
|
||||||
|
trace_db_call("query", "enhance_job_recent_prompts", |_span| {
|
||||||
|
let mut conn = self.connection.lock().expect("Unable to get EnhanceJobDao");
|
||||||
|
let names = serde_json::to_string(templates.unwrap_or(&[]))
|
||||||
|
.map_err(|e| anyhow::anyhow!("Encode error: {}", e))?;
|
||||||
|
// `template` and `MAX(created_at)` are bare/aggregate in the same
|
||||||
|
// group, so SQLite takes `template` from the newest row.
|
||||||
|
sql_query(
|
||||||
|
"SELECT TRIM(prompt, ' ' || char(9) || char(10) || char(13)) AS prompt, \
|
||||||
|
template, MAX(created_at) AS last_used, COUNT(*) AS uses \
|
||||||
|
FROM enhance_jobs \
|
||||||
|
WHERE prompt IS NOT NULL \
|
||||||
|
AND TRIM(prompt, ' ' || char(9) || char(10) || char(13)) <> '' \
|
||||||
|
AND (? = 0 OR template IN (SELECT value FROM json_each(?))) \
|
||||||
|
GROUP BY TRIM(prompt, ' ' || char(9) || char(10) || char(13)) \
|
||||||
|
ORDER BY last_used DESC, MAX(rowid) DESC LIMIT ?",
|
||||||
|
)
|
||||||
|
.bind::<Integer, _>(i32::from(templates.is_some()))
|
||||||
|
.bind::<Text, _>(names)
|
||||||
|
.bind::<BigInt, _>(limit)
|
||||||
|
.load::<RecentPrompt>(&mut *conn)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
|
||||||
|
})
|
||||||
|
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
|
||||||
|
}
|
||||||
|
|
||||||
fn discard(&mut self, id: &str) -> Result<(), DbError> {
|
fn discard(&mut self, id: &str) -> Result<(), DbError> {
|
||||||
trace_db_call("update", "enhance_job_discard", |_span| {
|
trace_db_call("update", "enhance_job_discard", |_span| {
|
||||||
let mut conn = self.connection.lock().expect("Unable to get EnhanceJobDao");
|
let mut conn = self.connection.lock().expect("Unable to get EnhanceJobDao");
|
||||||
@@ -358,4 +411,59 @@ mod tests {
|
|||||||
assert!(d.get("c").unwrap().is_some());
|
assert!(d.get("c").unwrap().is_some());
|
||||||
assert!(d.get("b").unwrap().is_none());
|
assert!(d.get("b").unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_job(id: &str, template: &str, prompt: Option<&str>, created: i64) -> EnhanceJobRow {
|
||||||
|
EnhanceJobRow {
|
||||||
|
template: template.into(),
|
||||||
|
prompt: prompt.map(str::to_string),
|
||||||
|
..job(id, "h", "ready", created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recent_prompts_groups_trimmed_newest_first() {
|
||||||
|
let mut d = dao();
|
||||||
|
d.insert(&prompt_job("a", "edit", Some("warmer"), 100))
|
||||||
|
.unwrap();
|
||||||
|
d.insert(&prompt_job("b", "edit-fast", Some(" warmer\n"), 300))
|
||||||
|
.unwrap();
|
||||||
|
d.insert(&prompt_job("c", "edit", Some("remove the sign"), 200))
|
||||||
|
.unwrap();
|
||||||
|
d.insert(&prompt_job("d", "edit", Some(" "), 400))
|
||||||
|
.unwrap();
|
||||||
|
d.insert(&prompt_job("e", "edit", None, 500)).unwrap();
|
||||||
|
d.insert(&prompt_job("f", "restore", Some("older"), 50))
|
||||||
|
.unwrap();
|
||||||
|
// Failed and discarded jobs still count.
|
||||||
|
d.update("b", &JobUpdate::status("failed")).unwrap();
|
||||||
|
d.discard("a").unwrap();
|
||||||
|
|
||||||
|
let all = d.recent_prompts(None, 10).unwrap();
|
||||||
|
let view: Vec<_> = all
|
||||||
|
.iter()
|
||||||
|
.map(|p| (p.prompt.as_str(), p.template.as_str(), p.last_used, p.uses))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
view,
|
||||||
|
[
|
||||||
|
("warmer", "edit-fast", 300, 2),
|
||||||
|
("remove the sign", "edit", 200, 1),
|
||||||
|
("older", "restore", 50, 1),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let edit = d
|
||||||
|
.recent_prompts(Some(&["edit".into(), "nope".into()]), 10)
|
||||||
|
.unwrap();
|
||||||
|
let view: Vec<_> = edit.iter().map(|p| (p.prompt.as_str(), p.uses)).collect();
|
||||||
|
assert_eq!(view, [("remove the sign", 1), ("warmer", 1)]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
d.recent_prompts(Some(&["nope".into()]), 10)
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
assert!(d.recent_prompts(Some(&[]), 10).unwrap().is_empty());
|
||||||
|
assert_eq!(d.recent_prompts(None, 1).unwrap().len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//!
|
//!
|
||||||
//! - `GET /enhance/templates`
|
//! - `GET /enhance/templates`
|
||||||
//! - `POST /enhance`
|
//! - `POST /enhance`
|
||||||
|
//! - `GET /enhance/prompts?templates=&limit=`
|
||||||
//! - `GET /enhance/jobs?path=`
|
//! - `GET /enhance/jobs?path=`
|
||||||
//! - `GET /enhance/jobs/{id}`
|
//! - `GET /enhance/jobs/{id}`
|
||||||
//! - `GET /enhance/jobs/{id}/preview/{n}`
|
//! - `GET /enhance/jobs/{id}/preview/{n}`
|
||||||
@@ -363,6 +364,57 @@ pub async fn list_jobs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PromptsQuery {
|
||||||
|
/// Comma-separated template names; omitted = all templates.
|
||||||
|
pub templates: Option<String>,
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PromptView {
|
||||||
|
prompt: String,
|
||||||
|
template: String,
|
||||||
|
last_used: i64,
|
||||||
|
uses: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompts used before, newest first, so the app can offer them again for a
|
||||||
|
/// series of similar photos. DB only: not library-scoped, and answered even
|
||||||
|
/// when ComfyUI is down or Enhance is off (then simply from old rows).
|
||||||
|
#[get("/enhance/prompts")]
|
||||||
|
pub async fn recent_prompts(
|
||||||
|
_: Claims,
|
||||||
|
q: web::Query<PromptsQuery>,
|
||||||
|
svc: web::Data<EnhanceService>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let names: Option<Vec<String>> = q.templates.as_deref().map(|t| {
|
||||||
|
t.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|n| !n.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
// `templates=` with nothing in it means "no filter", like omitting it.
|
||||||
|
let names = names.filter(|n| !n.is_empty());
|
||||||
|
let limit = q.limit.unwrap_or(10).clamp(1, 50);
|
||||||
|
match svc.recent_prompts(names.as_deref(), limit) {
|
||||||
|
Ok(rows) => {
|
||||||
|
let prompts: Vec<PromptView> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| PromptView {
|
||||||
|
prompt: r.prompt,
|
||||||
|
template: r.template,
|
||||||
|
last_used: r.last_used,
|
||||||
|
uses: r.uses,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
HttpResponse::Ok().json(json!({ "prompts": prompts }))
|
||||||
|
}
|
||||||
|
Err(e) => error_response(&e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/enhance/jobs/{id}/preview/{n}")]
|
#[get("/enhance/jobs/{id}/preview/{n}")]
|
||||||
pub async fn preview(
|
pub async fn preview(
|
||||||
_: Claims,
|
_: Claims,
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ impl Fx {
|
|||||||
.service(super::get_templates)
|
.service(super::get_templates)
|
||||||
.service(super::submit)
|
.service(super::submit)
|
||||||
.service(super::list_jobs)
|
.service(super::list_jobs)
|
||||||
|
.service(super::recent_prompts)
|
||||||
.service(super::get_job)
|
.service(super::get_job)
|
||||||
.service(super::preview)
|
.service(super::preview)
|
||||||
.service(super::approve)
|
.service(super::approve)
|
||||||
@@ -991,3 +992,62 @@ async fn busy_queue_blocks_the_free() {
|
|||||||
fx.svc.idle_check().await;
|
fx.svc.idle_check().await;
|
||||||
assert_eq!(fx.mock.free_calls.load(Ordering::SeqCst), 0);
|
assert_eq!(fx.mock.free_calls.load(Ordering::SeqCst), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn recent_prompts_lists_distinct_trimmed_prompts_even_when_discarded() {
|
||||||
|
let fx = fx().await;
|
||||||
|
fx.put(PHOTO, b"photo");
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for prompt in ["warmer", " warmer ", "less grain"] {
|
||||||
|
let (st, job, _) = call(
|
||||||
|
&fx,
|
||||||
|
"POST",
|
||||||
|
"/enhance",
|
||||||
|
Some(json!({"path": PHOTO, "template": "restore", "prompt": prompt})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, 200);
|
||||||
|
ids.push(job["id"].as_str().unwrap().to_string());
|
||||||
|
}
|
||||||
|
call(
|
||||||
|
&fx,
|
||||||
|
"POST",
|
||||||
|
&format!("/enhance/jobs/{}/discard", ids[0]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (st, body, _) = call(&fx, "GET", "/enhance/prompts", None).await;
|
||||||
|
assert_eq!(st, 200);
|
||||||
|
let rows = body["prompts"].as_array().unwrap();
|
||||||
|
let texts: Vec<(&str, i64)> = rows
|
||||||
|
.iter()
|
||||||
|
.map(|r| (r["prompt"].as_str().unwrap(), r["uses"].as_i64().unwrap()))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(texts.iter().map(|t| t.1).sum::<i64>(), 3);
|
||||||
|
assert!(texts.contains(&("warmer", 2)) && texts.contains(&("less grain", 1)));
|
||||||
|
assert_eq!(rows[0]["template"], "restore");
|
||||||
|
assert!(rows[0]["last_used"].as_i64().unwrap() > 0);
|
||||||
|
|
||||||
|
let (_, body, _) = call(&fx, "GET", "/enhance/prompts?templates=nope", None).await;
|
||||||
|
assert_eq!(body, json!({"prompts": []}));
|
||||||
|
let (_, body, _) = call(
|
||||||
|
&fx,
|
||||||
|
"GET",
|
||||||
|
"/enhance/prompts?templates=nope,restore&limit=0",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
body["prompts"].as_array().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"limit clamps to 1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn recent_prompts_answers_when_enhance_is_off() {
|
||||||
|
let fx = fx_with(None, |c| c.url = None).await;
|
||||||
|
let (st, body, _) = call(&fx, "GET", "/enhance/prompts", None).await;
|
||||||
|
assert_eq!((st, body), (200, json!({"prompts": []})));
|
||||||
|
}
|
||||||
+3
-1
@@ -21,5 +21,7 @@ pub mod template;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
|
|
||||||
pub use handlers::{approve, discard, get_job, get_templates, list_jobs, preview, submit};
|
pub use handlers::{
|
||||||
|
approve, discard, get_job, get_templates, list_jobs, preview, recent_prompts, submit,
|
||||||
|
};
|
||||||
pub use service::EnhanceService;
|
pub use service::EnhanceService;
|
||||||
+15
-2
@@ -18,8 +18,8 @@ use super::config::{EnhanceConfig, Loaded, PromptMode};
|
|||||||
use super::path_map::PathMap;
|
use super::path_map::PathMap;
|
||||||
use super::template::Template;
|
use super::template::Template;
|
||||||
use crate::database::enhance_dao::{
|
use crate::database::enhance_dao::{
|
||||||
EnhanceJobDao, EnhanceJobRow, JobUpdate, STATUS_APPROVED, STATUS_EXPIRED, STATUS_FAILED,
|
EnhanceJobDao, EnhanceJobRow, JobUpdate, RecentPrompt, STATUS_APPROVED, STATUS_EXPIRED,
|
||||||
STATUS_QUEUED, STATUS_READY, STATUS_RUNNING, STATUS_UNAVAILABLE,
|
STATUS_FAILED, STATUS_QUEUED, STATUS_READY, STATUS_RUNNING, STATUS_UNAVAILABLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Short timeout for the `available` probe, so the templates call stays snappy
|
/// Short timeout for the `available` probe, so the templates call stays snappy
|
||||||
@@ -392,6 +392,19 @@ impl EnhanceService {
|
|||||||
.map_err(|e| EnhanceError::Internal(e.to_string()))
|
.map_err(|e| EnhanceError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Distinct recent prompts; DB only, so it works with ComfyUI down.
|
||||||
|
pub fn recent_prompts(
|
||||||
|
&self,
|
||||||
|
templates: Option<&[String]>,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<RecentPrompt>, EnhanceError> {
|
||||||
|
self.dao
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.recent_prompts(templates, limit)
|
||||||
|
.map_err(|e| EnhanceError::Internal(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Hide a job from the per-photo list. Nothing is deleted on ComfyUI:
|
/// Hide a job from the per-photo list. Nothing is deleted on ComfyUI:
|
||||||
/// staged results vanish on its restart anyway.
|
/// staged results vanish on its restart anyway.
|
||||||
pub fn discard(&self, id: &str) -> Result<EnhanceJobRow, EnhanceError> {
|
pub fn discard(&self, id: &str) -> Result<EnhanceJobRow, EnhanceError> {
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
.service(enhance::get_templates)
|
.service(enhance::get_templates)
|
||||||
.service(enhance::submit)
|
.service(enhance::submit)
|
||||||
.service(enhance::list_jobs)
|
.service(enhance::list_jobs)
|
||||||
|
.service(enhance::recent_prompts)
|
||||||
.service(enhance::get_job)
|
.service(enhance::get_job)
|
||||||
.service(enhance::preview)
|
.service(enhance::preview)
|
||||||
.service(enhance::approve)
|
.service(enhance::approve)
|
||||||
|
|||||||
Reference in new issue
Block a user