diff --git a/README.md b/README.md
index cb10b95..f792076 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,23 @@ to each image on your local file system. The results will go in `output.csv`.
For more details, run `classify-images --help`.
+Tips & Tricks
+-------------
+
+It can be hard to remember the exact format for template files. localturk can help! Run it with
+the `--write-template` argument to generate a template file for your input that you can edit:
+
+ localturk --write-template tasks.csv > template.html
+
+When you're going through many tasks, keyboard shortcuts can speed things up tremendously.
+localturk supports these via the `data-key` attribute on form elements. For example, make yourer
+submit button look like this:
+
+
+
+Now, when you press `d`, it'll automatically click the "Good" button for you. _Note that this
+feature is not available on mechanical turk itself!_
+
Development
-----------
diff --git a/classify-images.ts b/classify-images.ts
index 198693c..4cd4620 100644
--- a/classify-images.ts
+++ b/classify-images.ts
@@ -18,6 +18,8 @@ import * as escape from 'escape-html';
import * as fs from 'fs';
import * as program from 'commander';
+import {dedent} from './utils';
+
const temp = require('temp').track();
function list(val) {
@@ -41,10 +43,11 @@ if (program.args.length == 0) {
}
if (fs.existsSync(program.output)) {
- console.warn(`Output file ${program.output} already exists.
-Its contents will be assumed to be previously-generated labels.
-If you want to start from scratch, either delete this file,
-rename it or specify a different output via --output`);
+ console.warn(dedent`
+ Output file ${program.output} already exists.
+ Its contents will be assumed to be previously-generated labels.
+ If you want to start from scratch, either delete this file,
+ rename it or specify a different output via --output`);
}
const csvInfo = temp.openSync({suffix: '.csv'});
@@ -55,47 +58,27 @@ fs.closeSync(csvInfo.fd);
const buttonsHtml = program.labels.map((label, idx) => {
const buttonText = `${label} (${1 + idx})`;
- return ``;
+ return ``;
}).join(' ');
const widthHtml = program.max_width ? ` width="${program.max_width}"` : '';
-const undoHtml = `
-
-
`;
+const undoHtml = dedent`
+
+ `;
let html = buttonsHtml + undoHtml + '\n';
// Add keyboard shortcuts. 1=first button, etc.
-html += `
-
-`;
+html += dedent`
+ `;
fs.writeSync(templateInfo.fd, html);
fs.closeSync(templateInfo.fd);
-const args = ['localturk', '-q', '--static_dir', '.', templateInfo.path, csvInfo.path, program.output];
+const args = ['localturk', '--static-dir', '.', templateInfo.path, csvInfo.path, program.output];
console.log('Running ', args.join(' '));
child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'});
diff --git a/localturk.ts b/localturk.ts
index 9bb6067..dbf5590 100644
--- a/localturk.ts
+++ b/localturk.ts
@@ -18,6 +18,7 @@ import open = require('open');
import * as _ from 'lodash';
import * as csv from './csv';
+import {makeTemplate} from './sample-template';
import * as utils from './utils';
import { outputFile } from 'fs-extra';
@@ -25,16 +26,27 @@ program
.version('2.0.0')
.usage('[options] template.html tasks.csv outputs.csv')
.option('-p, --port ', 'Run on this port (default 4321)', parseInt)
+ .option('-s, --static-dir ',
+ 'Serve static content from this directory. Default is same directory as template file.')
.option('-w, --write-template', 'Generate a stub template file based on the input CSV.')
.parse(process.argv);
-const {args} = program;
-if (3 !== args.length) {
+const {args, writeTemplate} = program;
+if (!((3 === args.length && !writeTemplate) ||
+ (1 === args.length && writeTemplate))) {
program.help();
}
+if (writeTemplate) {
+ // tasks.csv is the only input with --write-template.
+ args.unshift('');
+ args.push('');
+}
const [templateFile, tasksFile, outputsFile] = args;
const port = program.port || 4321;
+// --static-dir is particularly useful for classify-images, where the template file is in a
+// temporary directory but the image files could be anywhere.
+const staticDir = program['staticDir'] || path.dirname(templateFile);
type Task = {[key: string]: string};
let flash = ''; // this is used to show warnings in the web UI.
@@ -45,6 +57,7 @@ async function renderTemplate({task, numCompleted, numTotal}: TaskStats) {
for (const k in task) {
fullDict[k] = utils.htmlEntities(task[k]);
}
+ // Note: these two fields are not available in mechanical turk.
fullDict['ALL_JSON'] = utils.htmlEntities(JSON.stringify(task, null, 2));
fullDict['ALL_JSON_RAW'] = JSON.stringify(task);
const userHtml = utils.renderTemplate(template, fullDict);
@@ -56,19 +69,31 @@ async function renderTemplate({task, numCompleted, numTotal}: TaskStats) {
``
).join('\n');
- return `
-
-
-${numCompleted} / ${numTotal} - localturk
-
-
-
-`;
+ return utils.dedent`
+
+
+ ${numCompleted} / ${numTotal} - localturk
+
+
+
+
+ `;
}
async function readCompletedTasks(): Promise {
@@ -119,15 +144,10 @@ async function getNextTask(): Promise {
}
}
-if (program['write-template']) {
- // TODO(danvk): implement.
- process.exit(0);
-}
-
const app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(errorhandler());
-app.use(express.static(path.resolve(path.dirname(templateFile))));
+app.use(express.static(path.resolve(staticDir)));
app.get('/', utils.wrapPromise(async (req, res) => {
const nextTask = await getNextTask();
@@ -155,7 +175,17 @@ app.post('/delete-last', utils.wrapPromise(async (req, res) => {
res.redirect('/');
}));
-app.listen(port);
-const url = `http://localhost:${port}`;
-console.log('Running local turk on', url);
-open(url);
+
+if (writeTemplate) {
+ (async () => {
+ const columns = await csv.readHeaders(tasksFile);
+ console.log(makeTemplate(columns));
+ })().catch(e => {
+ console.error(e);
+ });
+} else {
+ app.listen(port);
+ const url = `http://localhost:${port}`;
+ console.log('Running local turk on', url);
+ open(url);
+}
diff --git a/sample-template.ts b/sample-template.ts
new file mode 100644
index 0000000..5f068f4
--- /dev/null
+++ b/sample-template.ts
@@ -0,0 +1,18 @@
+import {dedent, htmlEntities} from './utils';
+
+/**
+ * Write out a sample template file for a given input CSV.
+ */
+export function makeTemplate(columnNames: string[]) {
+ const inputs = columnNames.map(column => column + ': ${' + column + '}');
+ return dedent`
+ ${inputs.join(' \n ')}
+
+
+
+
+ `;
+}
diff --git a/utils.ts b/utils.ts
index 606d55c..6b1e67e 100644
--- a/utils.ts
+++ b/utils.ts
@@ -53,3 +53,24 @@ export function wrapPromise(
});
};
}
+
+/**
+ * Removes leading indents from a template string without removing all leading whitespace.
+ * Taken from tslint.
+ */
+export function dedent(strings: TemplateStringsArray, ...values: any[]) {
+ let fullString = strings.reduce((accumulator, str, i) => accumulator + values[i - 1] + str);
+
+ // match all leading spaces/tabs at the start of each line
+ const match = fullString.match(/^[ \t]*(?=\S)/gm);
+ if (!match) {
+ // e.g. if the string is empty or all whitespace.
+ return fullString;
+ }
+
+ // find the smallest indent, we don't want to remove all leading whitespace
+ const indent = Math.min(...match.map(el => el.length));
+ const regexp = new RegExp('^[ \\t]{' + indent + '}', 'gm');
+ fullString = indent > 0 ? fullString.replace(regexp, '') : fullString;
+ return fullString;
+}